diff --git a/pom.xml b/pom.xml
index 2611d06..69aee81 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,8 +32,7 @@
2.28.0
2.21.0
4.0.0
-
- 7.5.1
+ 6.0.0-j8
2.17.1
2.0.17
3.14.0
@@ -85,9 +84,9 @@
${settings.version}
- org.testng
- testng
- ${testng.version}
+ com.nordstrom.tools
+ testng-foundation
+ ${testng-foundation.version}
test
diff --git a/src/test/java/com/nordstrom/remote/SshUtilsTest.java b/src/test/java/com/nordstrom/remote/SshUtilsTest.java
index 0f37692..012b2e6 100644
--- a/src/test/java/com/nordstrom/remote/SshUtilsTest.java
+++ b/src/test/java/com/nordstrom/remote/SshUtilsTest.java
@@ -15,7 +15,6 @@
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
-import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.Arrays;
import java.util.Base64;
@@ -31,53 +30,48 @@
import org.apache.sshd.server.channel.ChannelSession;
import org.apache.sshd.server.command.Command;
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.jcraft.jsch.Buffer;
import com.jcraft.jsch.JSch;
import com.nordstrom.remote.RemoteConfig.RemoteSettings;
-public class SshUtilsTest {
- private SshServer sshd;
- private int port;
- private Path mockRemoteRoot;
- private PublicKey clientPublicKey;
- private Path clientPrivateKeyPath;
+public class SshUtilsTest extends TestNgBase {
private final String USER = "tester";
private final String PASS = "password123";
- @BeforeClass
+ @BeforeMethod
public void startServer() throws Exception {
- sshd = SshServer.setUpDefaultServer();
- sshd.setPort(0);
+ setSshServer(SshServer.setUpDefaultServer());
+ getSshServer().setPort(0);
// 2. Setup Host Key (The server's identity)
KeyPairGenerator hostGen = KeyPairGenerator.getInstance("RSA");
hostGen.initialize(2048);
KeyPair hostPair = hostGen.generateKeyPair();
- sshd.setKeyPairProvider(KeyPairProvider.wrap(hostPair));
+ getSshServer().setKeyPairProvider(KeyPairProvider.wrap(hostPair));
generateClientIdentity();
// 3. Integrated Authenticators
- sshd.setPasswordAuthenticator((u, p, s) -> USER.equals(u) && PASS.equals(p));
+ getSshServer().setPasswordAuthenticator((u, p, s) -> USER.equals(u) && PASS.equals(p));
- sshd.setPublickeyAuthenticator((u, key, s) -> {
+ getSshServer().setPublickeyAuthenticator((u, key, s) -> {
if (!USER.equals(u)) return false;
// Compare encoded byte arrays to avoid object-type mismatches
- return Arrays.equals(key.getEncoded(), this.clientPublicKey.getEncoded());
+ return Arrays.equals(key.getEncoded(), getPublicKey().getEncoded());
});
- // Requires both publickey AND password to succeed
- sshd.getProperties().put(CoreModuleProperties.AUTH_METHODS.getName(), "publickey,password");
+ // Requires both public key AND password to succeed
+ getSshServer().getProperties().put(CoreModuleProperties.AUTH_METHODS.getName(), "publickey,password");
- mockRemoteRoot = Files.createTempDirectory("ssh_remote_root");
- sshd.setFileSystemFactory(new VirtualFileSystemFactory(mockRemoteRoot));
+ setRemoteRoot(Files.createTempDirectory("ssh_remote_root"));
+ getSshServer().setFileSystemFactory(new VirtualFileSystemFactory(getRemoteRoot()));
- sshd.setCommandFactory((channel, command) -> new Command() {
+ getSshServer().setCommandFactory((channel, command) -> new Command() {
private OutputStream out;
private ExitCallback callback;
@@ -106,30 +100,25 @@ public void start(ChannelSession channel, Environment env) throws IOException {
@Override public void setErrorStream(OutputStream err) {}
});
- sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
+ getSshServer().setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
- sshd.start();
- port = sshd.getPort();
- updateKnownHostsWithActualPort(this.port, hostPair);
+ getSshServer().start();
+ setServerPort(getSshServer().getPort());
+ updateKnownHostsWithActualPort(getServerPort(), hostPair);
}
- @AfterClass(alwaysRun = true)
+ @AfterMethod
public void stopServer() throws IOException {
try {
- if (sshd != null && sshd.isStarted()) {
- sshd.close(true).await(1000);
+ if (getSshServer() != null && getSshServer().isStarted()) {
+ getSshServer().close(true).await(1000);
System.out.println("[INFO] Mock SSH Server stopped successfully.");
}
- if (clientPrivateKeyPath != null) {
- Files.walk(clientPrivateKeyPath.getParent())
- .sorted(Comparator.reverseOrder())
- .map(Path::toFile)
- .forEach(File::delete);
- }
} catch (IOException e) {
System.err.println("[WARN] SSH Server shutdown interrupted: " + e.getMessage());
} finally {
- recursiveDelete(mockRemoteRoot);
+ recursiveDelete(getPrivateKeyPath());
+ recursiveDelete(getRemoteRoot());
}
}
@@ -147,8 +136,8 @@ private void recursiveDelete(final Path path) {
@Test
public void testExecCommand() {
- String remoteUri = String.format("ssh://%s:%s@localhost:%d", USER, PASS, port);
- System.setProperty(RemoteSettings.SSH_KEY_NAME.key(), this.clientPrivateKeyPath.toString());
+ String remoteUri = String.format("ssh://%s:%s@localhost:%d", USER, PASS, getServerPort());
+ System.setProperty(RemoteSettings.SSH_KEY_NAME.key(), getPrivateKeyPath().toString());
String result = SshUtils.exec(remoteUri, "echo 'Remote-Session-Test'");
assertEquals(result.trim(), "Remote-Session-Test", "Command result mismatch");
@@ -160,11 +149,11 @@ public void testSftpUpload() throws IOException {
Files.write(localPath, "Hello SFTP".getBytes());
String sourceUri = localPath.toUri().toString();
- String remoteUri = String.format("ssh://%s:%s@localhost:%d", USER, PASS, port);
- System.setProperty(RemoteSettings.SSH_KEY_NAME.key(), this.clientPrivateKeyPath.toString());
+ String remoteUri = String.format("ssh://%s:%s@localhost:%d", USER, PASS, getServerPort());
+ System.setProperty(RemoteSettings.SSH_KEY_NAME.key(), getPrivateKeyPath().toString());
SshUtils.sftp(sourceUri, remoteUri);
- Path expectedFileOnServer = mockRemoteRoot.resolve(localPath.getFileName());
+ Path expectedFileOnServer = getRemoteRoot().resolve(localPath.getFileName());
assertTrue(Files.exists(expectedFileOnServer), "File should exist on the mock server");
String uploadedContent = new String(Files.readAllBytes(expectedFileOnServer));
assertEquals(uploadedContent, "Hello SFTP", "Content mismatch on remote server!");
@@ -174,14 +163,14 @@ public void testSftpUpload() throws IOException {
private void generateClientIdentity() throws Exception {
Path tempDir = Files.createTempDirectory("ssh_identity");
- this.clientPrivateKeyPath = tempDir.resolve("id_rsa_test");
+ setPrivateKeyPath(tempDir.resolve("id_rsa_test"));
JSch jsch = new JSch();
// 1. Generate the pair using JSch's generator
com.jcraft.jsch.KeyPair kpair = com.jcraft.jsch.KeyPair.genKeyPair(jsch, com.jcraft.jsch.KeyPair.RSA, 2048);
// 2. Write the Private Key in PEM format (Crucial for JSch to read it later)
- try (OutputStream os = new FileOutputStream(clientPrivateKeyPath.toFile())) {
+ try (OutputStream os = new FileOutputStream(getPrivateKeyPath().toFile())) {
kpair.writePrivateKey(os);
}
@@ -194,13 +183,13 @@ private void generateClientIdentity() throws Exception {
byte[] n = buf.getMPInt();
RSAPublicKeySpec spec = new RSAPublicKeySpec(new BigInteger(1, n), new BigInteger(1, e));
- this.clientPublicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
+ setPublicKey(KeyFactory.getInstance("RSA").generatePublic(spec));
kpair.dispose();
}
private void updateKnownHostsWithActualPort(int actualPort, KeyPair hostPair) throws IOException {
- Path knownHostsPath = clientPrivateKeyPath.resolveSibling("known_hosts");
+ Path knownHostsPath = getPrivateKeyPath().resolveSibling("known_hosts");
String encodedKey = Base64.getEncoder().encodeToString(hostPair.getPublic().getEncoded());
String entry = String.format("[localhost]:%d ssh-rsa %s%n", actualPort, encodedKey);
Files.write(knownHostsPath, entry.getBytes(StandardCharsets.UTF_8));
diff --git a/src/test/java/com/nordstrom/remote/TestNgBase.java b/src/test/java/com/nordstrom/remote/TestNgBase.java
new file mode 100644
index 0000000..942b7d7
--- /dev/null
+++ b/src/test/java/com/nordstrom/remote/TestNgBase.java
@@ -0,0 +1,155 @@
+package com.nordstrom.remote;
+
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.util.Optional;
+
+import org.apache.sshd.server.SshServer;
+import org.testng.ITestResult;
+import org.testng.Reporter;
+
+import com.nordstrom.automation.testng.ExecutionFlowController;
+import com.nordstrom.automation.testng.LinkedListeners;
+import com.nordstrom.automation.testng.TrackedObject;
+
+@LinkedListeners({ExecutionFlowController.class})
+public abstract class TestNgBase {
+
+ /**
+ * This enumeration is responsible for storing and retrieving values in the attributes collection of the current
+ * test result, as reported by {@link Reporter#getCurrentTestResult()}.
+ */
+ private enum TestAttribute {
+ SSH_SERVER("SshServer"),
+ SERVER_PORT("ServerPort"),
+ REMOTE_ROOT("RemoteRoot"),
+ PUBLIC_KEY("PublicKey"),
+ PRIVATE_KEY_PATH("PrivateKeyPath");
+
+ private String key;
+
+ /**
+ * Constructor for TestAttribute enumeration
+ *
+ * @param key key for this constant
+ */
+ TestAttribute(final String key) {
+ this.key = key;
+ }
+
+ /**
+ * Store the specified object in the attributes collection.
+ *
+ * @param obj object to be stored; 'null' to discard value
+ */
+// private void set(final Object obj) {
+// ITestResult result = Reporter.getCurrentTestResult();
+// if (obj != null) {
+// result.setAttribute(key, obj);
+// } else {
+// result.removeAttribute(key);
+// }
+// }
+
+ /**
+ * Store the specified object in the attributes collection, tracking reference propagation.
+ *
+ * @param obj object to be stored; 'null' to discard value and release tracked references
+ */
+ private void track(final Object obj) {
+ ITestResult result = Reporter.getCurrentTestResult();
+ if (obj != null) {
+ new TrackedObject<>(result, key, obj);
+ } else {
+ Object val = result.getAttribute(key);
+ if (val instanceof TrackedObject) {
+ ((TrackedObject>) val).release();
+ } else {
+ result.removeAttribute(key);
+ }
+ }
+ }
+
+ /**
+ * If present, get the object from the attributes collection.
+ *
+ * @return (optional) stored object
+ */
+ private Optional> nab() {
+ Object obj;
+ ITestResult result = Reporter.getCurrentTestResult();
+ Object val = result.getAttribute(key);
+ if (val instanceof TrackedObject) {
+ obj = ((TrackedObject>) val).getValue();
+ } else {
+ obj = val;
+ }
+ return optionalOf(obj);
+ }
+ }
+
+ public SshServer getSshServer() {
+ return TestAttribute.SSH_SERVER.nab()
+ .map(SshServer.class::cast)
+ .orElseThrow(() -> new RuntimeException("Test attribute 'SSH_SERVER' not initialized"));
+ }
+
+ public void setSshServer(final SshServer sshServer) {
+ TestAttribute.SSH_SERVER.track(sshServer);
+ }
+
+ public Integer getServerPort() {
+ return TestAttribute.SERVER_PORT.nab()
+ .map(Integer.class::cast)
+ .orElseThrow(() -> new RuntimeException("Test attribute 'SERVER_PORT' not initialized"));
+ }
+
+ public void setServerPort(final Integer serverPort) {
+ TestAttribute.SERVER_PORT.track(serverPort);
+ }
+
+ public Path getRemoteRoot() {
+ return TestAttribute.REMOTE_ROOT.nab()
+ .map(Path.class::cast)
+ .orElseThrow(() -> new RuntimeException("Test attribute 'REMOTE_ROOT' not initialized"));
+ }
+
+ public void setRemoteRoot(final Path remoteRoot) {
+ TestAttribute.REMOTE_ROOT.track(remoteRoot);
+ }
+
+ public PublicKey getPublicKey() {
+ return TestAttribute.PUBLIC_KEY.nab()
+ .map(PublicKey.class::cast)
+ .orElseThrow(() -> new RuntimeException("Test attribute 'PUBLIC_KEY' not initialized"));
+ }
+
+ public void setPublicKey(final PublicKey publicKey) {
+ TestAttribute.PUBLIC_KEY.track(publicKey);
+ }
+
+ public Path getPrivateKeyPath() {
+ return TestAttribute.PRIVATE_KEY_PATH.nab()
+ .map(Path.class::cast)
+ .orElseThrow(() -> new RuntimeException("Test attribute 'PRIVATE_KEY_PATH' not initialized"));
+ }
+
+ public void setPrivateKeyPath(final Path privateKeyPath) {
+ TestAttribute.PRIVATE_KEY_PATH.track(privateKeyPath);
+ }
+
+ /**
+ * Wrap the specified object in an {@link Optional} object.
+ *
+ * @param type of object to be wrapped
+ * @param obj object to be wrapped (may be 'null')
+ * @return (optional) wrapped object; empty if {@code obj} is 'null'
+ */
+ public static Optional optionalOf(T obj) {
+ if (obj != null) {
+ return Optional.of(obj);
+ } else {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/src/test/resources/META-INF/services/org.testng.ITestNGListener b/src/test/resources/META-INF/services/org.testng.ITestNGListener
new file mode 100644
index 0000000..7f25195
--- /dev/null
+++ b/src/test/resources/META-INF/services/org.testng.ITestNGListener
@@ -0,0 +1 @@
+com.nordstrom.automation.testng.ListenerChain