From aa7c23d7fbb98d7d87f20e5a0ebab8698567f463 Mon Sep 17 00:00:00 2001 From: Scott Babcock Date: Thu, 9 Apr 2026 11:10:48 -0700 Subject: [PATCH] Switch to TestNG-Foundation --- pom.xml | 9 +- .../com/nordstrom/remote/SshUtilsTest.java | 77 ++++----- .../java/com/nordstrom/remote/TestNgBase.java | 155 ++++++++++++++++++ .../services/org.testng.ITestNGListener | 1 + 4 files changed, 193 insertions(+), 49 deletions(-) create mode 100644 src/test/java/com/nordstrom/remote/TestNgBase.java create mode 100644 src/test/resources/META-INF/services/org.testng.ITestNGListener 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