From f07ffdf24f0dbb4c6df77a02d6298e3f05e8044d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 11:14:46 +0100 Subject: [PATCH 1/9] HttpRequestStepCredentialsTest: use prettier random *.p12 temporary file names [JENKINS-70101] Signed-off-by: Jim Klimov --- .../plugins/http_request/HttpRequestStepCredentialsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java index f60b6428..129eeccf 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java @@ -72,7 +72,7 @@ private StandardCredentials getCertificateCredentialSimple() throws IOException if (p12simple == null) { // Contains a private key + openvpn certs, // as alias named "1" (according to keytool) - p12simple = File.createTempFile("test.p12", null, tmp); + p12simple = File.createTempFile("test-keystore-", ".p12", tmp); FileUtils.copyURLToFile(HttpRequestStepCredentialsTest.class.getResource("test.p12"), p12simple); } @@ -85,7 +85,7 @@ private StandardCredentials getCertificateCredentialTrusted() throws IOException if (p12trusted == null) { // Contains a private key + openvpn certs as alias named "1", // and another alias named "ca" with trustedKeyEntry for CA - p12trusted = File.createTempFile("testTrusted.p12", null, tmp); + p12trusted = File.createTempFile("testTrusted-keystore-", ".p12", tmp); FileUtils.copyURLToFile(HttpRequestStepCredentialsTest.class.getResource("testTrusted.p12"), p12trusted); } From d57e96230fb1125ab9bec8b08d2f1caa33ac3e89 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 11:26:56 +0100 Subject: [PATCH 2/9] HttpRequestStepCredentialsTest: update some javadocs, move code under shared property declarations and group test-case families [JENKINS-70101] Signed-off-by: Jim Klimov --- .../HttpRequestStepCredentialsTest.java | 126 +++++++++++++----- 1 file changed, 96 insertions(+), 30 deletions(-) diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java index 129eeccf..61d05bb5 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java @@ -37,27 +37,60 @@ import org.jvnet.hudson.test.junit.jupiter.WithJenkins; /** + * The HttpRequestStepCredentialsTest suite prepares pipeline scripts to + * retrieve some previously saved credentials, on the controller, + * on a node provided by it, and on a worker agent in separate JVM. + * This picks known-working test cases and their setup from other + * test classes which address those credential types in more detail.
+ * + * Initially tied to JENKINS-70101 research, and tests on remote agent + * would require + * PR credentials-plugin#391 + * to be merged first, so credentials-plugin processes {@code snapshot()} + * properly and readable keystore data gets to remote agent.
+ * + * So part of this is a mixed test suite of two plugins, making sure that + * the chosen versions do cooperate correctly for our ultimate needs.
+ * * @author Mark Waite + * @author Jim Klimov */ @WithJenkins class HttpRequestStepCredentialsTest extends HttpRequestTestBase { - // For developers: set to `true` so that pipeline console logs show - // up in System.out (and/or System.err) of the plugin test run by - // mvn test -Dtest="HttpRequestStepCredentialsTest" + /** For developers: set to `true` so that pipeline console logs show + * up in {@link System#out} (and/or {@link System#err}) of the plugin + * test run executed by: + *
+     *   mvn test -Dtest="HttpRequestStepCredentialsTest"
+     * 
+ */ private final boolean verbosePipelines = false; - private String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - f.getLogText().writeLogTo(0, baos); - return baos.toString(); - } - - // From CertificateCredentialImplTest + // From CertificateCredentialImplTest in credentials-plugin + /** Temporary location for keystore files. + * @see #p12simple + * @see #p12trusted + */ @TempDir private File tmp; + + /** A temporary (randomly-named) file with a PKCS#12 key/cert store which + * contains a private key + openvpn certs, as alias named "1" + * (according to keytool). + */ private File p12simple; + + /** A temporary (randomly-named) file with a PKCS#12 key/cert store which + * contains a private key + openvpn certs as alias named "1", + * and another alias named "ca" with trustedKeyEntry for CA + * (according to keytool). + */ private File p12trusted; + /** Reference to the system credentials provider, prepared by + * {@link #enableSystemCredentialsProvider} method + * before each test case. + */ private CredentialsStore store = null; private static StandardCredentials getInvalidCredential() throws FormException { @@ -108,6 +141,11 @@ void enableSystemCredentialsProvider() { assertThat("The system credentials provider is enabled", store, notNullValue()); } + ///////////////////////////////////////////////////////////////// + // Test cases + ///////////////////////////////////////////////////////////////// + + /** A credentials tracking test */ @Test void trackCredentials() throws Exception { StandardCredentials credential = getInvalidCredential(); @@ -162,19 +200,28 @@ void trackCredentials() throws Exception { assertThat(page.getAnchorByText(proj.getFullDisplayName()), notNullValue()); } - // A set of tests with certificate credentials in different contexts - // TODO: Test on remote agent as in https://github.com/jenkinsci/credentials-plugin/pull/391 - // but this requires that PR to be merged first, so credentials-plugin - // processes snapshot() and readable keystore data gets to remote agent. - // Note that the tests below focus on ability of the plugin to load and - // process the key store specified by the credential, rather than that - // it is usable further. It would be a separate effort to mock up a web - // server protected by HTTPS and using certificates for login (possibly - // user and server backed by two different CA's), and query that. + ///////////////////////////////////////////////////////////////// + // Helpers for pipeline tests about credentials retrievability + // by http-request-plugin (on same or remote JVM) + ///////////////////////////////////////////////////////////////// + + private String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + f.getLogText().writeLogTo(0, baos); + return baos.toString(); + } + + /** Returns a String with prepared part of the pipeline script with a request + * (to non-existent site) using a credential named by "id" parameter.
+ * + * Note: we accept any outcome for the HTTP request (for this plugin, unresolved + * host is HTTP-404) but it may not crash making use of the credential.
+ * + * @param id Credential ID, saved earlier into the store + * @param runnerTag Currently not used + * @return String with prepared part of pipeline script + */ private static String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) { - // Note: we accept any outcome (for the plugin, unresolved host is HTTP-404) - // but it may not crash making use of the credential - // Note: cases withLocalCertLookup also need cpsScriptCredentialTestImports() return "def authentication='" + id + "';\n" + "\n" + "echo \"Querying HTTPS with credential...\"\n" @@ -201,11 +248,27 @@ private static String cpsScriptCredentialTestHttpRequest(String id, String runne + "\n"; } + ///////////////////////////////////////////////////////////////// + // Certificate credentials retrievability by http-request-plugin + // in a local JVM (should work with all versions of credentials plugin) + ///////////////////////////////////////////////////////////////// + + // A set of tests with certificate credentials in different contexts + // TODO: Test on remote agent as in https://github.com/jenkinsci/credentials-plugin/pull/391 + // but this requires that PR to be merged first, so credentials-plugin + // processes snapshot() and readable keystore data gets to remote agent. + // Note that the tests below focus on ability of the plugin to load and + // process the key store specified by the credential, rather than that + // it is usable further. It would be a separate effort to mock up a web + // server protected by HTTPS and using certificates for login (possibly + // user and server backed by two different CA's), and query that. + + /** Check that "simple" Certificate credentials are usable with pipeline script + * running without a {@code node{}} block. + */ @Test @Issue({"JENKINS-70000", "JENKINS-70101"}) void testCertSimpleHttpRequestOnController() throws Exception { - // Check that credentials are usable with pipeline script - // running without a node{} StandardCredentials credential = getCertificateCredentialSimple(); store.addCredentials(Domain.global(), credential); @@ -232,11 +295,12 @@ void testCertSimpleHttpRequestOnController() throws Exception { j.assertLogContains("Treating UnknownHostException", run); } + /** Check that "simple" Certificate credentials are usable with pipeline script + * running on a {@code node{}} (provided by the controller JVM). + */ @Test @Issue({"JENKINS-70000", "JENKINS-70101"}) void testCertSimpleHttpRequestOnNodeLocal() throws Exception { - // Check that credentials are usable with pipeline script - // running on a node{} (provided by the controller) StandardCredentials credential = getCertificateCredentialSimple(); store.addCredentials(Domain.global(), credential); @@ -265,11 +329,12 @@ void testCertSimpleHttpRequestOnNodeLocal() throws Exception { j.assertLogContains("Treating UnknownHostException", run); } + /** Check that "trusted" Certificate credentials are usable with pipeline script + * running without a {@code node{}} block. + */ @Test @Issue({"JENKINS-70000", "JENKINS-70101"}) void testCertTrustedHttpRequestOnController() throws Exception { - // Check that credentials are usable with pipeline script - // running without a node{} StandardCredentials credential = getCertificateCredentialTrusted(); store.addCredentials(Domain.global(), credential); @@ -293,11 +358,12 @@ void testCertTrustedHttpRequestOnController() throws Exception { j.assertLogContains("Treating UnknownHostException", run); } + /** Check that "trusted" Certificate credentials are usable with pipeline script + * running on a {@code node{}} (provided by the controller JVM). + */ @Test @Issue({"JENKINS-70000", "JENKINS-70101"}) void testCertTrustedHttpRequestOnNodeLocal() throws Exception { - // Check that credentials are usable with pipeline script - // running on a node{} (provided by the controller) StandardCredentials credential = getCertificateCredentialTrusted(); store.addCredentials(Domain.global(), credential); From 5e2b2df9caf7e54e6048f00592368a86c432567d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 12:45:39 +0100 Subject: [PATCH 3/9] HttpRequestStepCredentialsTest: cpsScriptCredentialTestHttpRequest(): refactor to use verbosePipelines test class property [JENKINS-70101] Signed-off-by: Jim Klimov --- .../HttpRequestStepCredentialsTest.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java index 61d05bb5..51459454 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java @@ -214,6 +214,10 @@ private String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException /** Returns a String with prepared part of the pipeline script with a request * (to non-existent site) using a credential named by "id" parameter.
* + * The test class property {@link #verbosePipelines} can be used to toggle writing + * a copy of the progress message(s) to {@link System#out} and {@link System#err} + * of the build agent JVM.
+ * * Note: we accept any outcome for the HTTP request (for this plugin, unresolved * host is HTTP-404) but it may not crash making use of the credential.
* @@ -221,10 +225,13 @@ private String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException * @param runnerTag Currently not used * @return String with prepared part of pipeline script */ - private static String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) { + private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) { return "def authentication='" + id + "';\n" + "\n" - + "echo \"Querying HTTPS with credential...\"\n" + + "def msg\n" + + "\n" + + "msg = \"Querying HTTPS with credential...\"\n" + + "echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + "def response = httpRequest(url: 'https://github.xcom/api/v3',\n" + " httpMode: 'GET',\n" + " authentication: authentication,\n" @@ -235,7 +242,8 @@ private static String cpsScriptCredentialTestHttpRequest(String id, String runne + "println('First HTTP Request Plugin Status: '+ response.getStatus())\n" + "println('First HTTP Request Plugin Response: '+ response.getContent())\n" + "\n" - + "echo \"Querying HTTPS with credential again (reentrability)...\"\n" + + "msg = \"Querying HTTPS with credential again (reentrability)...\"\n" + + "echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + "response = httpRequest(url: 'https://github.xcom/api/v3',\n" + " httpMode: 'GET',\n" + " authentication: authentication,\n" From bbe2227acb2473d0f1638cec66426912cdaef6a9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 13:36:41 +0100 Subject: [PATCH 4/9] HttpRequestStepCredentialsTest: cpsScriptCredentialTestHttpRequest(): refactor to optionally juggle withReentrability and to debug-trace withLocalCertLookup [JENKINS-70101] Import and adapt code evicted from https://github.com/jenkinsci/credentials-plugin/pull/391 Signed-off-by: Jim Klimov --- .../HttpRequestStepCredentialsTest.java | 87 +++++++++++++++---- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java index 51459454..592c6a77 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java @@ -211,6 +211,21 @@ private String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException return baos.toString(); } + /** Returns a String with prepared part of the pipeline script with imports used by some other snippet generators */ + private String cpsScriptCredentialTestImports() { + return "import com.cloudbees.plugins.credentials.CredentialsMatchers;\n" + + "import com.cloudbees.plugins.credentials.CredentialsProvider;\n" + + "import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;\n" + + "import com.cloudbees.plugins.credentials.common.StandardCredentials;\n" + + "import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;\n" + + "import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;\n" + + "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl;\n" + + "import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl.KeyStoreSource;\n" + + "import hudson.security.ACL;\n" + + "import java.security.KeyStore;\n" + + "\n"; + } + /** Returns a String with prepared part of the pipeline script with a request * (to non-existent site) using a credential named by "id" parameter.
* @@ -223,12 +238,39 @@ private String getLogAsStringPlaintext(WorkflowRun f) throws java.io.IOException * * @param id Credential ID, saved earlier into the store * @param runnerTag Currently not used + * @param withReentrability If true, generate a second request with same credential, + * to make sure it is not garbled etc. by first use. + * @param withLocalCertLookup If true, add lookup and logging of keystore data + * (into the pipeline build console, optionally also system streams). + * Note: test cases {@code withLocalCertLookup} need to + * generate {@link #cpsScriptCredentialTestImports} into + * their pipelines first. * @return String with prepared part of pipeline script */ - private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) { + private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag, Boolean withReentrability, Boolean withLocalCertLookup) { return "def authentication='" + id + "';\n" + "\n" + "def msg\n" + + (withLocalCertLookup ? ( + "if (true) { // scoping\n" + + " msg = \"Finding credential...\"\n" + + " echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + + " StandardCredentials credential = CredentialsMatchers.firstOrNull(\n" + + " CredentialsProvider.lookupCredentials(\n" + + " StandardCredentials.class,\n" + + " Jenkins.instance, null, null),\n" + + " CredentialsMatchers.withId(authentication));\n" + + " msg = \"Getting keystore...\"\n" + + " echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + + " KeyStore keyStore = credential.getKeyStore();\n" + + " msg = \"Getting keystore source...\"\n" + + " echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + + " KeyStoreSource kss = ((CertificateCredentialsImpl) credential).getKeyStoreSource();\n" + + " msg = \"Getting keystore source bytes...\"\n" + + " echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + + " byte[] kssb = kss.getKeyStoreBytes();\n" + + "}\n" ) + : "" ) + "\n" + "msg = \"Querying HTTPS with credential...\"\n" + "echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" @@ -239,21 +281,36 @@ private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) { + " contentType : 'APPLICATION_FORM',\n" + " validResponseCodes: '100:599',\n" + " quiet: false)\n" - + "println('First HTTP Request Plugin Status: '+ response.getStatus())\n" - + "println('First HTTP Request Plugin Response: '+ response.getContent())\n" + + "println('" + (withReentrability ? "First " : "") + "HTTP Request Plugin Status: '+ response.getStatus())\n" + + "println('" + (withReentrability ? "First " : "") + "First HTTP Request Plugin Response: '+ response.getContent())\n" + "\n" - + "msg = \"Querying HTTPS with credential again (reentrability)...\"\n" - + "echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" - + "response = httpRequest(url: 'https://github.xcom/api/v3',\n" - + " httpMode: 'GET',\n" - + " authentication: authentication,\n" - + " consoleLogResponseBody: true,\n" - + " contentType : 'APPLICATION_FORM',\n" - + " validResponseCodes: '100:599',\n" - + " quiet: false)\n" - + "println('Second HTTP Request Plugin Status: '+ response.getStatus())\n" - + "println('Second HTTP Request Plugin Response: '+ response.getContent())\n" - + "\n"; + + (withReentrability ? ( + "msg = \"Querying HTTPS with credential again (reentrability)...\"\n" + + "echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + + "response = httpRequest(url: 'https://github.xcom/api/v3',\n" + + " httpMode: 'GET',\n" + + " authentication: authentication,\n" + + " consoleLogResponseBody: true,\n" + + " contentType : 'APPLICATION_FORM',\n" + + " validResponseCodes: '100:599',\n" + + " quiet: false)\n" + + "println('Second HTTP Request Plugin Status: '+ response.getStatus())\n" + + "println('Second HTTP Request Plugin Response: '+ response.getContent())\n" + + "\n" ) + : "" ); + } + + /** Wrapper for {@link #cpsScriptCredentialTestHttpRequest(String, String, Boolean, Boolean)} + * to MAYBE trace {@code withLocalCertLookup=verbosePipelines} by default */ + private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag, Boolean withReentrability) { + return cpsScriptCredentialTestHttpRequest(id, runnerTag, withReentrability, verbosePipelines); + } + + /** Wrapper for {@link #cpsScriptCredentialTestHttpRequest(String, String, Boolean, Boolean)} + * to MAYBE trace {@code withLocalCertLookup=verbosePipelines} + * and enable {@code withReentrability=true} by default */ + private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) { + return cpsScriptCredentialTestHttpRequest(id, runnerTag, true, verbosePipelines); } ///////////////////////////////////////////////////////////////// From d62350250dd35e8fe607c4761d0eb74ac307a172 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 14:32:18 +0100 Subject: [PATCH 5/9] HttpRequestStepCredentialsTest: refactor existing testCert*() to use updated cpsScriptCredentialTestHttpRequest() helper, and import their siblings from credentials-plugin#391 [JENKINS-70101] Signed-off-by: Jim Klimov --- .../HttpRequestStepCredentialsTest.java | 106 +++++++++++++++++- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java index 592c6a77..43db2047 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java @@ -102,6 +102,10 @@ private static StandardCredentials getInvalidCredential() throws FormException { } private StandardCredentials getCertificateCredentialSimple() throws IOException { + return getCertificateCredentialSimple("cred_cert_simple", "password"); + } + + private StandardCredentials getCertificateCredentialSimple(String id, String password) throws IOException { if (p12simple == null) { // Contains a private key + openvpn certs, // as alias named "1" (according to keytool) @@ -111,10 +115,14 @@ private StandardCredentials getCertificateCredentialSimple() throws IOException SecretBytes uploadedKeystore = SecretBytes.fromRawBytes(Files.readAllBytes(p12simple.toPath())); CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(null, uploadedKeystore); - return new CertificateCredentialsImpl(null, "cred_cert_simple", null, "password", storeSource); + return new CertificateCredentialsImpl(null, id, null, password, storeSource); } private StandardCredentials getCertificateCredentialTrusted() throws IOException { + return getCertificateCredentialTrusted("cred_cert_with_ca", "password"); + } + + private StandardCredentials getCertificateCredentialTrusted(String id, String password) throws IOException { if (p12trusted == null) { // Contains a private key + openvpn certs as alias named "1", // and another alias named "ca" with trustedKeyEntry for CA @@ -124,7 +132,26 @@ private StandardCredentials getCertificateCredentialTrusted() throws IOException SecretBytes uploadedKeystore = SecretBytes.fromRawBytes(Files.readAllBytes(p12trusted.toPath())); CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(null, uploadedKeystore); - return new CertificateCredentialsImpl(null, "cred_cert_with_ca", null, "password", storeSource); + return new CertificateCredentialsImpl(null, id, null, password, storeSource); + } + + /** Get a new CertificateCredentialsImpl() and save it into the {@link #store} */ + private void prepareUploadedKeystore(String id, String password, Boolean useSimple) throws IOException { + StandardCredentials c = (useSimple + ? getCertificateCredentialSimple(id, password) + : getCertificateCredentialTrusted(id, password) + ); + SystemCredentialsProvider.getInstance().getCredentials().add(c); + SystemCredentialsProvider.getInstance().save(); + } + + private void prepareUploadedKeystore(String id, String password) throws IOException { + prepareUploadedKeystore(id, password, true); + } + + // Partially from certificate-plugin CertificateCredentialImplTest setup() + private void prepareUploadedKeystore() throws IOException { + prepareUploadedKeystore("myCert", "password"); } @BeforeEach @@ -313,6 +340,12 @@ private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag) { return cpsScriptCredentialTestHttpRequest(id, runnerTag, true, verbosePipelines); } + /** Wrapper for {@link #cpsScriptCredentialTestHttpRequest(String, String, Boolean, Boolean)} + * to use a certificate credential {@code id="myCert"} and trace {@code withLocalCertLookup=true} */ + private String cpsScriptCertCredentialTestHttpRequest(String runnerTag) { + return cpsScriptCredentialTestHttpRequest("myCert", runnerTag, false, true); + } + ///////////////////////////////////////////////////////////////// // Certificate credentials retrievability by http-request-plugin // in a local JVM (should work with all versions of credentials plugin) @@ -340,7 +373,8 @@ void testCertSimpleHttpRequestOnController() throws Exception { // Configure the build to use the credential WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); String script = - cpsScriptCredentialTestHttpRequest("cred_cert_simple", "CONTROLLER BUILT-IN"); + cpsScriptCredentialTestImports() + + cpsScriptCredentialTestHttpRequest("cred_cert_simple", "CONTROLLER BUILT-IN", true, true); proj.setDefinition(new CpsFlowDefinition(script, false)); // Execute the build @@ -372,8 +406,9 @@ void testCertSimpleHttpRequestOnNodeLocal() throws Exception { // Configure the build to use the credential WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); String script = + cpsScriptCredentialTestImports() + "node {\n" + - cpsScriptCredentialTestHttpRequest("cred_cert_simple", "CONTROLLER NODE") + + cpsScriptCredentialTestHttpRequest("cred_cert_simple", "CONTROLLER NODE", true, true) + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -406,7 +441,8 @@ void testCertTrustedHttpRequestOnController() throws Exception { // Configure the build to use the credential WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); String script = - cpsScriptCredentialTestHttpRequest("cred_cert_with_ca", "CONTROLLER BUILT-IN"); + cpsScriptCredentialTestImports() + + cpsScriptCredentialTestHttpRequest("cred_cert_with_ca", "CONTROLLER BUILT-IN", true, true); proj.setDefinition(new CpsFlowDefinition(script, false)); // Execute the build @@ -435,8 +471,9 @@ void testCertTrustedHttpRequestOnNodeLocal() throws Exception { // Configure the build to use the credential WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); String script = + cpsScriptCredentialTestImports() + "node {\n" + - cpsScriptCredentialTestHttpRequest("cred_cert_with_ca", "CONTROLLER NODE") + + cpsScriptCredentialTestHttpRequest("cred_cert_with_ca", "CONTROLLER NODE", true, true) + "}\n"; proj.setDefinition(new CpsFlowDefinition(script, false)); @@ -454,4 +491,61 @@ void testCertTrustedHttpRequestOnNodeLocal() throws Exception { j.assertLogContains("Treating UnknownHostException", run); } + /** Simplified version of simple/trusted tests with "myCert" credential id, + * transplanted from https://github.com/jenkinsci/credentials-plugin/pull/391 : + * Check that Certificate credentials are usable with pipeline script + * running without a {@code node{}} block. + */ + @Test + @Issue("JENKINS-70101") + void testCertHttpRequestOnController() throws Exception { + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + cpsScriptCredentialTestImports() + + cpsScriptCertCredentialTestHttpRequest("CONTROLLER BUILT-IN"); + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + j.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + j.assertLogContains("HTTP Request Plugin Response: ", run); + j.assertLogContains("Using authentication: myCert", run); + } + + /** Simplified version of simple/trusted tests with "myCert" credential id, + * transplanted from https://github.com/jenkinsci/credentials-plugin/pull/391 : + * Check that Certificate credentials are usable with pipeline script + * running on a {@code node{}} (provided by the controller) + */ + @Test + @Issue("JENKINS-70101") + void testCertHttpRequestOnNodeLocal() throws Exception { + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + cpsScriptCredentialTestImports() + + "node {\n" + + cpsScriptCertCredentialTestHttpRequest("CONTROLLER NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + j.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + j.assertLogContains("HTTP Request Plugin Response: ", run); + j.assertLogContains("Using authentication: myCert", run); + } } From cd78b3a40babb6c1dc35d5d64c9bcaa42bec3d45 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 14:39:34 +0100 Subject: [PATCH 6/9] HttpRequestStepCredentialsTest: cpsScriptCredentialTestHttpRequest() helper: make use of runnerTag argument [JENKINS-70101] Signed-off-by: Jim Klimov --- .../plugins/http_request/HttpRequestStepCredentialsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java index 43db2047..5d70b2ab 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java @@ -264,7 +264,7 @@ private String cpsScriptCredentialTestImports() { * host is HTTP-404) but it may not crash making use of the credential.
* * @param id Credential ID, saved earlier into the store - * @param runnerTag Currently not used + * @param runnerTag Reported in pipeline build log * @param withReentrability If true, generate a second request with same credential, * to make sure it is not garbled etc. by first use. * @param withLocalCertLookup If true, add lookup and logging of keystore data @@ -299,7 +299,7 @@ private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag, B + "}\n" ) : "" ) + "\n" - + "msg = \"Querying HTTPS with credential...\"\n" + + "msg = \"Querying HTTPS with credential on " + (runnerTag != null ? runnerTag : "") + "...\"\n" + "echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + "def response = httpRequest(url: 'https://github.xcom/api/v3',\n" + " httpMode: 'GET',\n" From fd88890c333064637bce9bc52459fc85fc83f47a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 14:48:50 +0100 Subject: [PATCH 7/9] HttpRequestStepCredentialsTest: cpsScriptCredentialTestHttpRequest() helper: report cred ID if drilling withLocalCertLookup [JENKINS-70101] Signed-off-by: Jim Klimov --- .../plugins/http_request/HttpRequestStepCredentialsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java index 5d70b2ab..be9d385c 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java @@ -280,7 +280,7 @@ private String cpsScriptCredentialTestHttpRequest(String id, String runnerTag, B + "def msg\n" + (withLocalCertLookup ? ( "if (true) { // scoping\n" - + " msg = \"Finding credential...\"\n" + + " msg = \"Finding credential with id='${authentication}'...\"\n" + " echo msg;" + (verbosePipelines ? " System.out.println(msg); System.err.println(msg)" : "" ) + ";\n" + " StandardCredentials credential = CredentialsMatchers.firstOrNull(\n" + " CredentialsProvider.lookupCredentials(\n" From 8bab8d84f71b31720c0dfa94c0ac4324279a3818 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 14:55:43 +0100 Subject: [PATCH 8/9] HttpRequestStepCredentialsTest: import tests and helper code to (optionally) verify that credentials pass over to remote agents correctly [JENKINS-70101] Signed-off-by: Jim Klimov --- .../HttpRequestStepCredentialsTest.java | 266 +++++++++++++++++- 1 file changed, 263 insertions(+), 3 deletions(-) diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java index be9d385c..eaef05e6 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; @@ -12,8 +13,12 @@ import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; + +import hudson.model.Descriptor; import hudson.model.Descriptor.FormException; import hudson.model.Fingerprint; +import hudson.model.Label; +import hudson.model.Node; import hudson.model.Result; import java.io.ByteArrayOutputStream; @@ -21,6 +26,8 @@ import java.io.IOException; import java.nio.file.Files; import java.util.Collections; + +import hudson.model.Slave; import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; @@ -66,6 +73,15 @@ class HttpRequestStepCredentialsTest extends HttpRequestTestBase { */ private final boolean verbosePipelines = false; + // Data for build agent setup + /** Build agent label expected by test cases for remote logic execution + * and data transfer [JENKINS-70101] */ + private final static String agentLabelString = "cred-test-worker"; + // Can this be reused for many test cases? + private Slave agent = null; + /** Tri-state Unknown/started/not usable [JENKINS-70101] */ + private Boolean agentUsable = null; + // From CertificateCredentialImplTest in credentials-plugin /** Temporary location for keystore files. * @see #p12simple @@ -93,6 +109,34 @@ class HttpRequestStepCredentialsTest extends HttpRequestTestBase { */ private CredentialsStore store = null; + /** True if we can use remote agent tests, and the credentials plugin version + * here is expected to transfer secret data across the Channel correctly + * (assuming issue JENKINS-70101 is fixed in that plugin). + */ + static private Boolean credentialsPluginDoesSnapshotsRight = null; + static { + try { + Class.forName( + "com.cloudbees.plugins.credentials.impl.CertificateCredentialsSnapshotTaker", + false, HttpRequestStepCredentialsTest.class.getClassLoader()); + credentialsPluginDoesSnapshotsRight = true; + } catch (ClassNotFoundException ignored) { + credentialsPluginDoesSnapshotsRight = false; + } catch (ExceptionInInitializerError ignored) { + // Per https://www.baeldung.com/java-check-class-exists the Class.forName() + // calls a static initializer which may fail (at least for the default + // single-argument version of the method), but still -- if we get that far, + // the class exists so we are probably running the version of plugin with + // https://github.com/jenkinsci/credentials-plugin/pull/391 + credentialsPluginDoesSnapshotsRight = true; + } + } + /** Honour the check via {@link #credentialsPluginDoesSnapshotsRight} [false], + * or try (and possibly fail) with any implementation/version of the + * credentials plugin [true]? + */ + private Boolean credentialsPluginTestRemoteAlways = false; + private static StandardCredentials getInvalidCredential() throws FormException { String username = "bad-user"; String password = "bad-password"; @@ -166,6 +210,9 @@ void enableSystemCredentialsProvider() { } } assertThat("The system credentials provider is enabled", store, notNullValue()); + + // Primarily needed for remote-agent tests per JENKINS-70101? + j.jenkins.setCrumbIssuer(null); } ///////////////////////////////////////////////////////////////// @@ -352,9 +399,10 @@ private String cpsScriptCertCredentialTestHttpRequest(String runnerTag) { ///////////////////////////////////////////////////////////////// // A set of tests with certificate credentials in different contexts - // TODO: Test on remote agent as in https://github.com/jenkinsci/credentials-plugin/pull/391 - // but this requires that PR to be merged first, so credentials-plugin - // processes snapshot() and readable keystore data gets to remote agent. + // NOTE: Test cases on remote agent require the PR + // https://github.com/jenkinsci/credentials-plugin/pull/391 + // to be merged first, so credentials-plugin processes snapshot() + // and readable keystore data gets to remote agent. // Note that the tests below focus on ability of the plugin to load and // process the key store specified by the credential, rather than that // it is usable further. It would be a separate effort to mock up a web @@ -491,6 +539,86 @@ void testCertTrustedHttpRequestOnNodeLocal() throws Exception { j.assertLogContains("Treating UnknownHostException", run); } + ///////////////////////////////////////////////////////////////// + // Helpers for pipeline tests with remote agents + ///////////////////////////////////////////////////////////////// + + private Boolean isAvailableAgent() { + // Can be used to skip optional tests if we know we could not set up an agent + if (agent == null) + return false; + return agentUsable; + } + + private Boolean setupAgent() throws OutOfMemoryError, Exception { + if (isAvailableAgent()) + return true; + + // See how credentialsPluginTestRemoteAlways is determined above + // and revise if the ultimately merged fix that started as + // https://github.com/jenkinsci/credentials-plugin/pull/391 + // gets changed before the merge or later on... + String msg_70101 = "This test needs a version of credentials-plugin with a fix for JENKINS-70101, and that does not seem to be deployed here"; + if (!credentialsPluginTestRemoteAlways) + assumeTrue(credentialsPluginDoesSnapshotsRight, msg_70101); + + // else: credentialsPluginTestRemoteAlways, even if we fail + if (!credentialsPluginDoesSnapshotsRight) { + System.err.println("WARNING: " + msg_70101 + "; this test run was configured to try remote agents anyway"); + // return false; + } + + // Note we anticipate this might fail e.g. due to system resources; + // it should not block the whole test suite from running + // (we would just dynamically skip certain test cases) + try { + // Define a "Permanent Agent" + Label agentLabel = Label.get(agentLabelString); + agent = j.createOnlineSlave(agentLabel); + agent.setNodeDescription("Worker in another JVM, remoting used"); + agent.setNumExecutors(1); + agent.setMode(Node.Mode.EXCLUSIVE); + ///agent.setRetentionStrategy(new RetentionStrategy.Always()); + +/* + // Add node envvars + List env = new ArrayList(); + env.add(new Entry("key1","value1")); + env.add(new Entry("key2","value2")); + EnvironmentVariablesNodeProperty envPro = new EnvironmentVariablesNodeProperty(env); + agent.getNodeProperties().add(envPro); +*/ + + String agentLog = null; + agentUsable = false; + for (long i = 0; i < 5; i++) { + Thread.sleep(1000); + agentLog = agent.getComputer().getLog(); + if (i == 2 && (agentLog == null || agentLog.isEmpty())) { + // Give it a little time to autostart, then kick it up if needed: + agent.getComputer().connect(true); // "always" should have started it; avoid duplicate runs + } + if (agentLog != null && agentLog.contains("Agent successfully connected and online")) { + agentUsable = true; + break; + } + } + System.out.println("Spawned build agent " + + "usability: " + agentUsable.toString() + + "; connection log:" + (agentLog == null ? " " : "\n" + agentLog)); + } catch (Descriptor.FormException | NullPointerException e) { + agentUsable = false; + } + + return agentUsable; + } + + ///////////////////////////////////////////////////////////////// + // Certificate credentials retrievability by http-request-plugin + // in a set of local+remote JVMs (should work with versions of + // credentials plugin where issue JENKINS-70101 is fixed) + ///////////////////////////////////////////////////////////////// + /** Simplified version of simple/trusted tests with "myCert" credential id, * transplanted from https://github.com/jenkinsci/credentials-plugin/pull/391 : * Check that Certificate credentials are usable with pipeline script @@ -548,4 +676,136 @@ void testCertHttpRequestOnNodeLocal() throws Exception { j.assertLogContains("HTTP Request Plugin Response: ", run); j.assertLogContains("Using authentication: myCert", run); } + + /** + * Check that Certificate credentials are usable with pipeline script + * running on a remote {@code node{}} with separate JVM (e.g. + * check that remoting and credential snapshot work properly). + */ + @Test + @Issue("JENKINS-70101") + void testCertHttpRequestOnNodeRemote() throws Exception { + assumeTrue(this.setupAgent() == true, "This test needs a separate build agent"); + + prepareUploadedKeystore(); + + // Configure the build to use the credential + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + cpsScriptCredentialTestImports() + + "node(\"" + agentLabelString + "\") {\n" + + cpsScriptCertCredentialTestHttpRequest("REMOTE NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + j.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + j.assertLogContains("HTTP Request Plugin Response: ", run); + } + + ///////////////////////////////////////////////////////////////// + // User/pass credentials tests + ///////////////////////////////////////////////////////////////// + + // Partially from UsernamePasswordCredentialsImplTest setup() + private void prepareUsernamePassword() throws IOException, FormException { + UsernamePasswordCredentialsImpl credentials = + new UsernamePasswordCredentialsImpl(null, + "abc123", "Bob’s laptop", + "bob", "s3cr3t"); + SystemCredentialsProvider.getInstance().getCredentials().add(credentials); + SystemCredentialsProvider.getInstance().save(); + } + + private String cpsScriptUsernamePasswordCredentialTestHttpRequest(String runnerTag) { + return cpsScriptCredentialTestHttpRequest("abc123", runnerTag, false); + } + + /** Check that Username credentials are usable with pipeline script + * running without a {@code node{}} block. + */ + @Test + @Issue("JENKINS-70101") + void testUsernamePasswordHttpRequestOnController() throws Exception { + prepareUsernamePassword(); + + // Configure the build to use the credential + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + cpsScriptUsernamePasswordCredentialTestHttpRequest("CONTROLLER BUILT-IN"); + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + j.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + j.assertLogContains("HTTP Request Plugin Response: ", run); + } + + /** Check that Username credentials are usable with pipeline script + * running on a {@code node{}} (provided by the controller JVM). + */ + @Test + @Issue("JENKINS-70101") + void testUsernamePasswordHttpRequestOnNodeLocal() throws Exception { + prepareUsernamePassword(); + + // Configure the build to use the credential + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + "node {\n" + + cpsScriptUsernamePasswordCredentialTestHttpRequest("CONTROLLER NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + j.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + j.assertLogContains("HTTP Request Plugin Response: ", run); + } + + /** + * Check that Username credentials are usable with pipeline script + * running on a remote {@code node{}} with separate JVM (e.g. + * check that remoting and credential snapshot work properly). + */ + @Test + @Issue("JENKINS-70101") + void testUsernamePasswordHttpRequestOnNodeRemote() throws Exception { + // Check that credentials are usable with pipeline script + // running on a remote node{} with separate JVM (check + // that remoting/snapshot work properly) + assumeTrue(this.setupAgent() == true, "This test needs a separate build agent"); + + prepareUsernamePassword(); + + // Configure the build to use the credential + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj"); + String script = + "node(\"" + agentLabelString + "\") {\n" + + cpsScriptUsernamePasswordCredentialTestHttpRequest("REMOTE NODE") + + "}\n"; + proj.setDefinition(new CpsFlowDefinition(script, false)); + + // Execute the build + WorkflowRun run = proj.scheduleBuild2(0).get(); + if (verbosePipelines) System.out.println(getLogAsStringPlaintext(run)); + + // Check expectations + j.assertBuildStatus(Result.SUCCESS, run); + // Got to the end? + j.assertLogContains("HTTP Request Plugin Response: ", run); + } } From 3bac1c6c48ee205cbf30bdc47d980ef4a4572bc8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 5 Jan 2026 17:24:13 +0100 Subject: [PATCH 9/9] HttpRequestStepCredentialsTest: use the simpler form of Class.forName(), it seems to be more reliable [JENKINS-70101] Signed-off-by: Jim Klimov --- .../plugins/http_request/HttpRequestStepCredentialsTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java index eaef05e6..c1e972fb 100644 --- a/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java +++ b/src/test/java/jenkins/plugins/http_request/HttpRequestStepCredentialsTest.java @@ -117,8 +117,9 @@ class HttpRequestStepCredentialsTest extends HttpRequestTestBase { static { try { Class.forName( - "com.cloudbees.plugins.credentials.impl.CertificateCredentialsSnapshotTaker", - false, HttpRequestStepCredentialsTest.class.getClassLoader()); + "com.cloudbees.plugins.credentials.impl.CertificateCredentialsSnapshotTaker" + // , false, HttpRequestStepCredentialsTest.class.getClassLoader() + ); credentialsPluginDoesSnapshotsRight = true; } catch (ClassNotFoundException ignored) { credentialsPluginDoesSnapshotsRight = false;