formData = Collections.emptyList();
+ public static final String outputFile = "";
+ public static final ResponseHandle responseHandle = ResponseHandle.STRING;
+
+ public DescriptorImpl() {
+ load();
+ }
+
+ @Override
+ public boolean isApplicable(Class extends AbstractProject> aClass) {
+ return true;
+ }
+
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return "HTTP Request";
+ }
+
+ @Override
+ public Publisher newInstance(StaplerRequest2 req, @NonNull JSONObject formData) throws FormException {
+ return (HttpRequestPublisher) super.newInstance(req, formData);
+ }
+
+ public ListBoxModel doFillHttpModeItems() {
+ return HttpMode.getPublisherFillItems();
+ }
+
+ public ListBoxModel doFillAcceptTypeItems() {
+ return MimeType.getContentTypeFillItems();
+ }
+
+ public ListBoxModel doFillContentTypeItems() {
+ return MimeType.getContentTypeFillItems();
+ }
+
+ public ListBoxModel doFillResponseHandleItems() {
+ ListBoxModel items = new ListBoxModel();
+ for (ResponseHandle responseHandle : ResponseHandle.values()) {
+ items.add(responseHandle.name());
+ }
+ return items;
+ }
+
+ public ListBoxModel doFillAuthenticationItems(@AncestorInPath Item project,
+ @QueryParameter String url) {
+ return HttpRequest.DescriptorImpl.fillAuthenticationItems(project, url);
+ }
+
+ public ListBoxModel doFillProxyAuthenticationItems(@AncestorInPath Item project,
+ @QueryParameter String url) {
+ return HttpRequest.DescriptorImpl.fillAuthenticationItems(project, url);
+ }
+
+ public FormValidation doCheckValidResponseCodes(@QueryParameter String value) {
+ return HttpRequest.DescriptorImpl.checkValidResponseCodes(value);
+ }
+ }
+}
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/config.jelly b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/config.jelly
new file mode 100644
index 00000000..f6a13a42
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/config.jelly
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-acceptType.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-acceptType.html
new file mode 100644
index 00000000..b317690b
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-acceptType.html
@@ -0,0 +1,3 @@
+
+ Add Accept: foo HTTP request header where foo is the HTTP content type to accept.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-authentication.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-authentication.html
new file mode 100644
index 00000000..37ac2015
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-authentication.html
@@ -0,0 +1,4 @@
+
+ Authentication that will be used before this request.
+ Authentications are created in global configuration under a key name that is selected here.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-consoleLogResponseBody.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-consoleLogResponseBody.html
new file mode 100644
index 00000000..5f5fb706
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-consoleLogResponseBody.html
@@ -0,0 +1,3 @@
+
+ This allows to turn off writing the response body to the log.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-contentType.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-contentType.html
new file mode 100644
index 00000000..71c3aa3c
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-contentType.html
@@ -0,0 +1,3 @@
+
+ Add Content-Type: foo HTTP request header where foo is the HTTP content type the request is using.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-httpMode.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-httpMode.html
new file mode 100644
index 00000000..c634fafb
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-httpMode.html
@@ -0,0 +1,3 @@
+
+ The HTTP mode of the request such as 'GET' or 'POST'.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-httpProxy.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-httpProxy.html
new file mode 100644
index 00000000..6a1d8965
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-httpProxy.html
@@ -0,0 +1,3 @@
+
+ Use a proxy to process the HTTP request. Example: http://mycorpproxy:80
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-ignoreSslErrors.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-ignoreSslErrors.html
new file mode 100644
index 00000000..3f7c4e89
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-ignoreSslErrors.html
@@ -0,0 +1,3 @@
+
+ If set to true, a request with HTTPS will trust in the certificate even when it is invalid or expired.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-multipartName.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-multipartName.html
new file mode 100644
index 00000000..1cde573e
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-multipartName.html
@@ -0,0 +1,3 @@
+
+ Multipart entity name used in the Content-Disposition header in conjunction with the upload file path.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-outputFile.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-outputFile.html
new file mode 100644
index 00000000..bfe88777
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-outputFile.html
@@ -0,0 +1,3 @@
+
+ Name of the file in which to write response data.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-quiet.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-quiet.html
new file mode 100644
index 00000000..15e9b000
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-quiet.html
@@ -0,0 +1,3 @@
+
+ This allows to turn off all logging output.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-requestBody.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-requestBody.html
new file mode 100644
index 00000000..ec453af2
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-requestBody.html
@@ -0,0 +1,4 @@
+
+
The raw body of the request.
+
Parameters will be resolved.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-responseHandle.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-responseHandle.html
new file mode 100644
index 00000000..4aaa541e
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-responseHandle.html
@@ -0,0 +1,7 @@
+
+ How response should be handled.
+ STRING(default) will transform the response in string.
+ NONE will not read the response content and will close the response after the job execution.
+ LEAVE_OPEN will not read the response content but leaves an open input stream to be read outside the job execution.
+ Using LEAVE_OPEN it is your responsibility to close it after use. 'response.close();'
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-timeout.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-timeout.html
new file mode 100644
index 00000000..37e4feb3
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-timeout.html
@@ -0,0 +1,3 @@
+
+ Specify a timeout value in seconds (default is 0 which implies no timeout).
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-uploadFile.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-uploadFile.html
new file mode 100644
index 00000000..2842e1c7
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-uploadFile.html
@@ -0,0 +1,5 @@
+
+ Path to the upload file, relative to build workspace or absolute path.
+ Can be used to upload a file as multipart/form-data POST request.
+ The according content type should be set above, defaults to application/octet-stream otherwise.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-url.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-url.html
new file mode 100644
index 00000000..b3e1f6a5
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-url.html
@@ -0,0 +1,3 @@
+
+ Specify an URL to be requested.
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-useSystemProperties.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-useSystemProperties.html
new file mode 100644
index 00000000..7691ddf8
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-useSystemProperties.html
@@ -0,0 +1,3 @@
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-validResponseCodes.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-validResponseCodes.html
new file mode 100644
index 00000000..6cc79cb9
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-validResponseCodes.html
@@ -0,0 +1,8 @@
+
+ Configure response code to mark an execution as success .
+ You can configure simple code such as "200" or multiple codes separeted by comma(',') e.g. "200,404,500"
+ Interval of codes should be in format From:To e.g. "100:399".
+
+ The default (as if empty) is to fail to 4xx and 5xx. That means success from 100 to 399 "100:399" .
+ To ignore any response code use "100:599".
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-validResponseContent.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-validResponseContent.html
new file mode 100644
index 00000000..f0ae7d2b
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-validResponseContent.html
@@ -0,0 +1,3 @@
+
+ If set response must contain this string to mark an execution as success .
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-wrapAsMultipart.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-wrapAsMultipart.html
new file mode 100644
index 00000000..3823b82e
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help-wrapAsMultipart.html
@@ -0,0 +1,4 @@
+
+ If set to false an upload file will be set directly as body of the request and will not be
+ wrapped as multipart/form-data .
+
diff --git a/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help.html b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help.html
new file mode 100644
index 00000000..b270da9a
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/http_request/HttpRequestPublisher/help.html
@@ -0,0 +1,24 @@
+
+
+ Performs an HTTP request, and returns a response object.
+
Usage example:
+
+ def response = httpRequest 'http://localhost:8080/jenkins/api/json?pretty=true'
+ println("Status: "+response.status)
+ println("Content: "+response.content)
+
+
If Jenkins restarts after the HTTP request is made, but before the HTTP response is received, the HTTP request fails.
+
validResponseCodes is a comma-separated string of single values or from:to ranges. For example '200' to accept only 200 or '201,301:303' to accept 201 as well as the range from 301 to 303 .
+
+
+ The methods of the response object are:
+
+
+ String getContent() : The HTTP Response entity. This means the text of the response without the headers. If the response does not contain an entity, getContent() returns null.
+
+
+ int getStatus() : The HTTP status code.
+
+
+
+
diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestFreestylePublisherTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestFreestylePublisherTest.java
new file mode 100644
index 00000000..0d35d748
--- /dev/null
+++ b/src/test/java/jenkins/plugins/http_request/HttpRequestFreestylePublisherTest.java
@@ -0,0 +1,181 @@
+package jenkins.plugins.http_request;
+
+import static jenkins.plugins.http_request.Registers.registerCheckRequestBody;
+import static jenkins.plugins.http_request.Registers.registerCustomHeaders;
+import static jenkins.plugins.http_request.Registers.registerRequestChecker;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
+
+import hudson.model.FreeStyleBuild;
+import hudson.model.FreeStyleProject;
+import hudson.model.Result;
+import hudson.util.ListBoxModel;
+
+import jenkins.plugins.http_request.util.HttpRequestNameValuePair;
+
+/**
+ * Exercises {@link HttpRequestPublisher} as a standalone post-build action, mirroring the
+ * build-step coverage in {@link HttpRequestTest} but wiring the request through
+ * {@link FreeStyleProject#getPublishersList()}.
+ *
+ * {@link HttpRequestPublisher#perform} does not reject any {@link HttpMode} at run time. The
+ * outbound-only allowlist ({@link HttpMode#SUPPORTED_HTTP_MODES}) is enforced purely at
+ * configuration time, by restricting the "HTTP mode" dropdown
+ * ({@link HttpRequestPublisher.DescriptorImpl#doFillHttpModeItems}) to those modes. A publisher
+ * configured with any other mode - including the GET default - still performs the request.
+ *
+ * @author James Boylan
+ */
+@WithJenkins
+class HttpRequestFreestylePublisherTest extends HttpRequestPublisherTestBase {
+
+ /**
+ * Adds {@code publisher} as a post-build action and runs the build to completion.
+ */
+ private FreeStyleBuild runPublisher(HttpRequestPublisher publisher) throws Exception {
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getPublishersList().add(publisher);
+ return project.scheduleBuild2(0).get();
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // Outbound HTTP modes offered by the dropdown: POST, PUT, DELETE, PATCH
+ // ---------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @EnumSource(mode = EnumSource.Mode.INCLUDE, names = {"POST", "PUT", "DELETE", "PATCH"})
+ void supportedModePerformsRequest(HttpMode mode) throws Exception {
+ // Prepare the server
+ registerRequestChecker(mode);
+
+ // Prepare HttpRequestPublisher
+ HttpRequestPublisher publisher = new HttpRequestPublisher(baseURL() + "/do" + mode.name());
+ publisher.setHttpMode(mode);
+ publisher.setConsoleLogResponseBody(true);
+
+ // Run build (as a post-build action)
+ FreeStyleBuild build = runPublisher(publisher);
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ /**
+ * The default {@link HttpMode} (GET) is not offered by the dropdown, but {@code perform} has no
+ * runtime allowlist check: a publisher left at defaults still performs the request.
+ */
+ @Test
+ void defaultModePerformsGetRequest() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequestPublisher (httpMode left at its GET default)
+ HttpRequestPublisher publisher = new HttpRequestPublisher(baseURL() + "/doGET");
+ publisher.setConsoleLogResponseBody(true);
+
+ // Run build (as a post-build action)
+ FreeStyleBuild build = runPublisher(publisher);
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // Full request handling from the publisher context
+ // ---------------------------------------------------------------------------------------------
+
+ @Test
+ void passesRequestBodyOnPost() throws Exception {
+ // Prepare the server
+ registerCheckRequestBody();
+
+ // Prepare HttpRequestPublisher
+ HttpRequestPublisher publisher = new HttpRequestPublisher(baseURL() + "/checkRequestBody");
+ publisher.setHttpMode(HttpMode.POST);
+ publisher.setRequestBody("TestRequestBody");
+ publisher.setConsoleLogResponseBody(true);
+
+ // Run build (as a post-build action)
+ FreeStyleBuild build = runPublisher(publisher);
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void sendsCustomHeaders() throws Exception {
+ // Prepare the server (the /customHeaders checker only answers GET)
+ registerCustomHeaders();
+
+ List customHeaders = new ArrayList<>();
+ customHeaders.add(new HttpRequestNameValuePair("customHeader", "value1"));
+ customHeaders.add(new HttpRequestNameValuePair("customHeader", "value2"));
+
+ HttpRequestPublisher publisher = new HttpRequestPublisher(baseURL() + "/customHeaders");
+ publisher.setConsoleLogResponseBody(true);
+ publisher.setCustomHeaders(customHeaders);
+
+ // Run build (as a post-build action)
+ FreeStyleBuild build = runPublisher(publisher);
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ }
+
+ @Test
+ void badResponseContentFailsTheBuild() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.POST);
+
+ // Prepare HttpRequestPublisher
+ HttpRequestPublisher publisher = new HttpRequestPublisher(baseURL() + "/doPOST");
+ publisher.setHttpMode(HttpMode.POST);
+ publisher.setConsoleLogResponseBody(true);
+ publisher.setValidResponseContent("bad content");
+
+ // Run build (as a post-build action)
+ FreeStyleBuild build = runPublisher(publisher);
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Fail: Response doesn't contain expected content 'bad content'", build);
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // Configuration-time allowlist: the dropdown only offers the outbound HTTP modes
+ // ---------------------------------------------------------------------------------------------
+
+ @Test
+ void httpModeDropdownOnlyOffersOutboundModes() {
+ HttpRequestPublisher.DescriptorImpl descriptor =
+ this.j.jenkins.getDescriptorByType(HttpRequestPublisher.DescriptorImpl.class);
+
+ Set offered = new HashSet<>();
+ for (ListBoxModel.Option option : descriptor.doFillHttpModeItems()) {
+ offered.add(option.value);
+ }
+
+ Set expected = HttpMode.SUPPORTED_HTTP_MODES.stream()
+ .map(Enum::name)
+ .collect(Collectors.toSet());
+
+ assertEquals(expected, offered);
+ }
+}
diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherRoundTripTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherRoundTripTest.java
new file mode 100644
index 00000000..ece13d2d
--- /dev/null
+++ b/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherRoundTripTest.java
@@ -0,0 +1,103 @@
+package jenkins.plugins.http_request;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
+
+import hudson.model.FreeStyleProject;
+
+import jenkins.plugins.http_request.util.HttpRequestNameValuePair;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies that {@link HttpRequestPublisher} survives a configuration round trip, and that the
+ * outbound-only HTTP-mode allowlist ({@link HttpMode#SUPPORTED_HTTP_MODES}) is enforced at
+ * configuration time via the "HTTP mode" dropdown rather than by rejecting a save or a build.
+ * @author James Boylan
+ */
+@WithJenkins
+class HttpRequestPublisherRoundTripTest {
+ List httpModes = Arrays.asList(HttpMode.POST, HttpMode.PUT, HttpMode.DELETE, HttpMode.PATCH);
+
+ @Test
+ void configRoundTrip(JenkinsRule j) throws Exception {
+
+ for (HttpMode mode : httpModes) {
+ HttpRequestPublisher reloaded = newRequest(j, mode);
+
+ assertEquals("http://domain/", reloaded.getUrl());
+ assertEquals(mode, reloaded.getHttpMode());
+ assertEquals("100:599", reloaded.getValidResponseCodes());
+ assertEquals("some content we want to see", reloaded.getValidResponseContent());
+ assertEquals(MimeType.TEXT_HTML, reloaded.getAcceptType());
+ assertEquals(MimeType.TEXT_HTML, reloaded.getContentType());
+ assertEquals("myfile.txt", reloaded.getOutputFile());
+ assertEquals(12, reloaded.getTimeout());
+ assertEquals(true, reloaded.getConsoleLogResponseBody());
+ assertEquals(1, reloaded.getCustomHeaders().size());
+ assertEquals("param1", reloaded.getCustomHeaders().get(0).getName());
+ assertEquals("value1", reloaded.getCustomHeaders().get(0).getValue());
+ }
+ }
+
+ private static HttpRequestPublisher newRequest(JenkinsRule j, HttpMode mode) throws Exception {
+ HttpRequestPublisher publisher = new HttpRequestPublisher("http://domain/");
+ publisher.setHttpMode(mode);
+ publisher.setValidResponseCodes("100:599");
+ publisher.setValidResponseContent("some content we want to see");
+ publisher.setAcceptType(MimeType.TEXT_HTML);
+ publisher.setContentType(MimeType.TEXT_HTML);
+ publisher.setOutputFile("myfile.txt");
+ publisher.setTimeout(12);
+ publisher.setConsoleLogResponseBody(true);
+ List customHeaders = new ArrayList<>();
+ customHeaders.add(new HttpRequestNameValuePair("param1", "value1"));
+ publisher.setCustomHeaders(customHeaders);
+
+ FreeStyleProject project = j.createFreeStyleProject();
+ project.getPublishersList().add(publisher);
+ j.configRoundtrip(project);
+
+ HttpRequestPublisher reloaded = project.getPublishersList().get(HttpRequestPublisher.class);
+ assertNotNull(reloaded);
+ return reloaded;
+ }
+
+ /**
+ * The "HTTP mode" dropdown only offers {@link HttpMode#SUPPORTED_HTTP_MODES}
+ * ({@link HttpMode#getPublisherFillItems}), so a publisher persisted with a mode outside that
+ * set (e.g. GET, the field default) has no matching option to preselect. Saving the config page
+ * as-is therefore submits whatever mode the browser defaults an unselected {@code } to -
+ * the first offered option - rather than failing; there is no runtime or save-time rejection.
+ */
+ @Test
+ void modeOutsideDropdownIsCoercedToFirstSupportedModeOnSave(JenkinsRule j) throws Exception {
+ HttpRequestPublisher publisher = new HttpRequestPublisher("http://domain/");
+ publisher.setHttpMode(HttpMode.GET);
+ FreeStyleProject project = j.createFreeStyleProject();
+ project.getPublishersList().add(publisher);
+
+ j.configRoundtrip(project);
+
+ HttpRequestPublisher reloaded = project.getPublishersList().get(HttpRequestPublisher.class);
+ assertNotNull(reloaded);
+ assertTrue(HttpMode.SUPPORTED_HTTP_MODES.contains(reloaded.getHttpMode()));
+ assertEquals(HttpMode.SUPPORTED_HTTP_MODES.iterator().next(), reloaded.getHttpMode());
+ }
+
+ @Test
+ void onlyOutboundIsSupported() {
+ for (HttpMode mode : httpModes) {
+ assertTrue(HttpMode.SUPPORTED_HTTP_MODES.contains(mode));
+ }
+ assertEquals(4, HttpMode.SUPPORTED_HTTP_MODES.size());
+ }
+}
diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherStepCredentialsTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherStepCredentialsTest.java
new file mode 100644
index 00000000..d3217b73
--- /dev/null
+++ b/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherStepCredentialsTest.java
@@ -0,0 +1,808 @@
+package jenkins.plugins.http_request;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collections;
+
+import org.apache.commons.io.FileUtils;
+import org.htmlunit.html.HtmlPage;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
+
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.CredentialsStore;
+import com.cloudbees.plugins.credentials.SecretBytes;
+import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
+import com.cloudbees.plugins.credentials.common.StandardCredentials;
+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.FormException;
+import hudson.model.Fingerprint;
+import hudson.model.Label;
+import hudson.model.Node;
+import hudson.model.Result;
+import hudson.model.Slave;
+import jenkins.model.Jenkins;
+
+/**
+ * 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 HttpRequestPublisherStepCredentialsTest extends HttpRequestPublisherTestBase {
+ /** 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;
+
+ // Data for build agent setup
+ /** Build agent label expected by test cases for remote logic execution
+ * and data transfer [JENKINS-70101] */
+ private static final 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
+ * @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;
+
+ /** 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";
+ CredentialsScope scope = CredentialsScope.GLOBAL;
+ String id = "username-" + username + "-password-" + password;
+ return new UsernamePasswordCredentialsImpl(scope, id, "desc: " + id, username, password);
+ }
+
+ 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)
+ p12simple = File.createTempFile("test-keystore-", ".p12", tmp);
+ FileUtils.copyURLToFile(HttpRequestPublisherStepCredentialsTest.class.getResource("test.p12"), p12simple);
+ }
+
+ SecretBytes uploadedKeystore = SecretBytes.fromRawBytes(Files.readAllBytes(p12simple.toPath()));
+ CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(null, uploadedKeystore);
+ 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
+ p12trusted = File.createTempFile("testTrusted-keystore-", ".p12", tmp);
+ FileUtils.copyURLToFile(HttpRequestPublisherStepCredentialsTest.class.getResource("testTrusted.p12"), p12trusted);
+ }
+
+ SecretBytes uploadedKeystore = SecretBytes.fromRawBytes(Files.readAllBytes(p12trusted.toPath()));
+ CertificateCredentialsImpl.UploadedKeyStoreSource storeSource = new CertificateCredentialsImpl.UploadedKeyStoreSource(null, uploadedKeystore);
+ return new CertificateCredentialsImpl(null, id, null, password, storeSource);
+ }
+
+ /** Get a new CertificateCredentialsImpl() and save it into the credentials 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
+ void enableSystemCredentialsProvider() {
+ SystemCredentialsProvider.getInstance()
+ .setDomainCredentialsMap(
+ Collections.singletonMap(Domain.global(), Collections.emptyList()));
+ for (CredentialsStore s : CredentialsProvider.lookupStores(Jenkins.get())) {
+ if (s.getProvider() instanceof SystemCredentialsProvider.ProviderImpl) {
+ store = s;
+ break;
+ }
+ }
+ assertThat("The system credentials provider is enabled", store, notNullValue());
+ }
+
+ /////////////////////////////////////////////////////////////////
+ // Test cases
+ /////////////////////////////////////////////////////////////////
+
+ /** A credentials tracking test */
+ @Test
+ void trackCredentials() throws Exception {
+ StandardCredentials credential = getInvalidCredential();
+ store.addCredentials(Domain.global(), credential);
+
+ Fingerprint fingerprint = CredentialsProvider.getFingerprintOf(credential);
+ assertThat("Fingerprint should not be set before job definition", fingerprint, nullValue());
+
+ JenkinsRule.WebClient wc = j.createWebClient();
+ HtmlPage page = wc.goTo("credentials/store/system/domain/_/credentials/" + credential.getId());
+ assertThat(
+ "No fingerprint created until first use on missing page",
+ page.getElementById("usage-missing"),
+ notNullValue());
+ assertThat(
+ "No fingerprint created until first use on present page",
+ page.getElementById("usage-present"),
+ nullValue());
+
+ // Configure the build to use the credential
+ WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj");
+ proj.setDefinition(
+ new CpsFlowDefinition(
+ "def response = httpRequest(url: 'https://api.github.com/users/jenkinsci',\n"
+ + " authentication: '" + credential.getId() + "')\n"
+ + "println('Status: '+response.getStatus())\n"
+ + "println('Response: '+response.getContent())\n",
+ true));
+
+ fingerprint = CredentialsProvider.getFingerprintOf(credential);
+ assertThat("Fingerprint should not be set before first build", fingerprint, nullValue());
+
+ // Execute the build
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+
+ // Check expectations
+ j.assertBuildStatus(Result.SUCCESS, run);
+ j.assertLogContains("https://api.github.com/users/jenkinsci/followers", run);
+
+ // Check the credential use was correctly tracked
+ fingerprint = CredentialsProvider.getFingerprintOf(credential);
+ assertThat("Fingerprint should be set after first build", fingerprint, notNullValue());
+ assertThat(fingerprint.getJobs(), hasItem(is(proj.getFullName())));
+ Fingerprint.RangeSet rangeSet = fingerprint.getRangeSet(proj);
+ assertThat(rangeSet, notNullValue());
+ assertThat(rangeSet.includes(proj.getLastBuild().getNumber()), is(true));
+
+ page = wc.goTo("credentials/store/system/domain/_/credentials/" + credential.getId());
+ assertThat(page.getElementById("usage-missing"), nullValue());
+ assertThat(page.getElementById("usage-present"), notNullValue());
+ assertThat(page.getAnchorByText(proj.getFullDisplayName()), notNullValue());
+ }
+
+ /////////////////////////////////////////////////////////////////
+ // Helpers for pipeline tests about credentials retrievability
+ // by http-request-plugin (on same or remote JVM)
+ /////////////////////////////////////////////////////////////////
+
+ private String getLogAsStringPlaintext(WorkflowRun f) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ f.getLogText().writeLogTo(0, baos);
+ 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.
+ *
+ * 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.
+ *
+ * @param id Credential ID, saved earlier into the store
+ * @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
+ * (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, Boolean withReentrability, Boolean withLocalCertLookup) {
+ return "def authentication='" + id + "';\n"
+ + "\n"
+ + "def msg\n"
+ + (withLocalCertLookup ? (
+ "if (true) { // scoping\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"
+ + " 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 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"
+ + " authentication: authentication,\n"
+ + " consoleLogResponseBody: true,\n"
+ + " contentType : 'APPLICATION_FORM',\n"
+ + " validResponseCodes: '100:599',\n"
+ + " quiet: false)\n"
+ + "println('" + (withReentrability ? "First " : "") + "HTTP Request Plugin Status: '+ response.getStatus())\n"
+ + "println('" + (withReentrability ? "First " : "") + "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);
+ }
+
+ /** 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)
+ /////////////////////////////////////////////////////////////////
+
+ // A set of tests with certificate credentials in different contexts
+ // 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
+ // 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 {
+ StandardCredentials credential = getCertificateCredentialSimple();
+ store.addCredentials(Domain.global(), credential);
+
+ // Configure the build to use the credential
+ WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj");
+ String script =
+ cpsScriptCredentialTestImports() +
+ cpsScriptCredentialTestHttpRequest("cred_cert_simple", "CONTROLLER BUILT-IN", true, true);
+ 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: cred_cert_simple", run);
+ // Currently we always try adding the material
+ // and report if not failed trying (might have
+ // had 0 entries to add though):
+ //j.assertLogNotContains("Added Trust Material from provided KeyStore", run);
+ j.assertLogContains("Added Key Material from provided KeyStore", run);
+ 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 {
+ StandardCredentials credential = getCertificateCredentialSimple();
+ store.addCredentials(Domain.global(), credential);
+
+ // 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", true, true) +
+ "}\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: cred_cert_simple", run);
+ // Currently we always try adding the material
+ // and report if not failed trying (might have
+ // had 0 entries to add though):
+ //j.assertLogNotContains("Added Trust Material from provided KeyStore", run);
+ j.assertLogContains("Added Key Material from provided KeyStore", run);
+ 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 {
+ StandardCredentials credential = getCertificateCredentialTrusted();
+ store.addCredentials(Domain.global(), credential);
+
+ // Configure the build to use the credential
+ WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proj");
+ String script =
+ cpsScriptCredentialTestImports() +
+ cpsScriptCredentialTestHttpRequest("cred_cert_with_ca", "CONTROLLER BUILT-IN", true, true);
+ 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: cred_cert_with_ca", run);
+ j.assertLogContains("Added Trust Material from provided KeyStore", run);
+ j.assertLogContains("Added Key Material from provided KeyStore", run);
+ 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 {
+ StandardCredentials credential = getCertificateCredentialTrusted();
+ store.addCredentials(Domain.global(), credential);
+
+ // 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", true, true) +
+ "}\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: cred_cert_with_ca", run);
+ j.assertLogContains("Added Trust Material from provided KeyStore", run);
+ j.assertLogContains("Added Key Material from provided KeyStore", run);
+ 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 (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
+ * 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);
+ }
+
+ /**
+ * 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);
+ }
+}
diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherTest.java b/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherTest.java
new file mode 100644
index 00000000..0222619a
--- /dev/null
+++ b/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherTest.java
@@ -0,0 +1,1171 @@
+package jenkins.plugins.http_request;
+
+import static jenkins.plugins.http_request.Registers.registerAcceptedTypeRequestChecker;
+import static jenkins.plugins.http_request.Registers.registerBasicAuth;
+import static jenkins.plugins.http_request.Registers.registerCheckBuildParameters;
+import static jenkins.plugins.http_request.Registers.registerCheckRequestBody;
+import static jenkins.plugins.http_request.Registers.registerCheckRequestBodyWithTag;
+import static jenkins.plugins.http_request.Registers.registerContentTypeRequestChecker;
+import static jenkins.plugins.http_request.Registers.registerCustomHeaders;
+import static jenkins.plugins.http_request.Registers.registerCustomHeadersResolved;
+import static jenkins.plugins.http_request.Registers.registerFileUpload;
+import static jenkins.plugins.http_request.Registers.registerFormAuth;
+import static jenkins.plugins.http_request.Registers.registerFormAuthBad;
+import static jenkins.plugins.http_request.Registers.registerInvalidStatusCode;
+import static jenkins.plugins.http_request.Registers.registerReqAction;
+import static jenkins.plugins.http_request.Registers.registerRequestChecker;
+import static jenkins.plugins.http_request.Registers.registerTimeout;
+import static jenkins.plugins.http_request.Registers.registerUnwrappedPutFileUpload;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.impl.classic.HttpResponseAdapter;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.Fields;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
+
+import hudson.Functions;
+import hudson.model.Cause.UserIdCause;
+import hudson.model.FreeStyleBuild;
+import hudson.model.FreeStyleProject;
+import hudson.model.ParametersAction;
+import hudson.model.ParametersDefinitionProperty;
+import hudson.model.Result;
+import hudson.model.StringParameterDefinition;
+import hudson.model.StringParameterValue;
+import jakarta.servlet.ServletException;
+
+import jenkins.plugins.http_request.auth.FormAuthentication;
+import jenkins.plugins.http_request.util.HttpRequestNameValuePair;
+import jenkins.plugins.http_request.util.RequestAction;
+
+/**
+ * @author Martin d'Anjou
+ */
+@WithJenkins
+class HttpRequestPublisherTest extends HttpRequestPublisherTestBase {
+
+ @TempDir
+ private File folder;
+
+ @Test
+ void simpleGetTest() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void quietTest() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setQuiet(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogNotContains("HttpMethod:", build);
+ this.j.assertLogNotContains("URL:", build);
+ this.j.assertLogNotContains("Sending request to url:", build);
+ this.j.assertLogNotContains("Response Code:", build);
+ }
+
+ @Test
+ void canDetectActualContent() throws Exception {
+ // Setup the expected pattern
+ String findMe = ALL_IS_WELL;
+
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setConsoleLogResponseBody(true);
+ httpRequest.setValidResponseContent(findMe);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(findMe, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void badContentFailsTheBuild() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setConsoleLogResponseBody(true);
+ httpRequest.setValidResponseContent("bad content");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Fail: Response doesn't contain expected content 'bad content'", build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void responseMatchAcceptedMimeType() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Expect a mime type that matches the response
+ httpRequest.setAcceptType(MimeType.TEXT_PLAIN);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void responseDoesNotMatchAcceptedMimeTypeDoesNotFailTheBuild() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Expect a mime type that does not match the response
+ httpRequest.setAcceptType(MimeType.TEXT_HTML);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void passBuildParametersWhenAskedAndParametersArePresent() throws Exception {
+ // Prepare the server
+ registerCheckBuildParameters();
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/checkBuildParameters");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Activate passBuildParameters
+ httpRequest.setPassBuildParameters(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.addProperty(new ParametersDefinitionProperty(
+ new StringParameterDefinition("foo", "default")
+ ));
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0, new UserIdCause(), new ParametersAction(new StringParameterValue("foo", "value"))).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void replaceParametersInRequestBody() throws Exception {
+
+ // Prepare the server
+ registerCheckRequestBodyWithTag();
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/checkRequestBodyWithTag");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Activate requsetBody
+ httpRequest.setHttpMode(HttpMode.POST);
+
+ // Use some random body content that contains a parameter
+ httpRequest.setRequestBody("cleanupDir=D:/continuousIntegration/deployments/Daimler/${Tag}/standalone");
+
+ // Build parameters have to be passed
+ httpRequest.setPassBuildParameters(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.addProperty(new ParametersDefinitionProperty(
+ new StringParameterDefinition("Tag", "default")
+ ));
+ project.getBuildersList().add(httpRequest);
+
+ FreeStyleBuild build = project.scheduleBuild2(0, new UserIdCause(), new ParametersAction(new StringParameterValue("Tag", "trunk"))).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void silentlyIgnoreNonExistentBuildParameters() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Activate passBuildParameters without parameters present
+ httpRequest.setPassBuildParameters(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void doNotPassBuildParametersWithBuildParameters() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Activate passBuildParameters
+ httpRequest.setPassBuildParameters(false);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0, new UserIdCause(), new ParametersAction(new StringParameterValue("foo", "value"))).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void passRequestBodyWhenRequestIsPostAndBodyIsPresent() throws Exception {
+ // Prepare the server
+ registerCheckRequestBody();
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/checkRequestBody");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Activate requsetBody
+ httpRequest.setHttpMode(HttpMode.POST);
+ httpRequest.setRequestBody("TestRequestBody");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void doNotPassRequestBodyWhenMethodIsGet() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Activate passBuildParameters
+ httpRequest.setRequestBody("TestRequestBody");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void doAllRequestTypes() throws Exception {
+ for (HttpMode method : HttpMode.values()) {
+ // Prepare the server
+ registerRequestChecker(method);
+ doRequest(method);
+
+ cleanHandlers();
+ }
+ }
+
+ private void doRequest(final HttpMode method) throws Exception {
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/do" + method.toString());
+ httpRequest.setHttpMode(method);
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+
+ if (method == HttpMode.HEAD) {
+ return;
+ }
+
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void invalidResponseCodeFailsTheBuild() throws Exception {
+ // Prepare the server
+ registerInvalidStatusCode();
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/invalidStatusCode");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Throwing status 400 for test", build);
+ this.j.assertLogContains("Fail: Status code 400 is not in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void invalidResponseCodeIsAccepted() throws Exception {
+ // Prepare the server
+ registerInvalidStatusCode();
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/invalidStatusCode");
+ httpRequest.setValidResponseCodes("100:599");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains("Throwing status 400 for test", build);
+ this.j.assertLogContains("Success: Status code 400 is in the accepted range: 100:599", build);
+ }
+
+ @Test
+ void reverseRangeFailsTheBuild() throws Exception {
+ // Prepare the server
+
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doesNotMatter");
+ httpRequest.setValidResponseCodes("599:100");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Interval 599:100 should be FROM less than TO", build);
+ }
+
+ @Test
+ void notANumberRangeValueFailsTheBuild() throws Exception {
+ // Prepare the server
+
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doesNotMatter");
+ httpRequest.setValidResponseCodes("text");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Invalid number text", build);
+ }
+
+ @Test
+ void rangeWithTextFailsTheBuild() throws Exception {
+ // Prepare the server
+
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doesNotMatter");
+ httpRequest.setValidResponseCodes("1:text");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Invalid number text", build);
+ }
+
+ @Test
+ void invalidRangeFailsTheBuild() throws Exception {
+ // Prepare the server
+
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doesNotMatter");
+ httpRequest.setValidResponseCodes("1:2:3");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Code 1:2:3 should be an interval from:to or a single value", build);
+ }
+
+ @Test
+ void sendAllContentTypes() {
+ for (MimeType mimeType : MimeType.values()) {
+ sendContentType(mimeType);
+ }
+ }
+
+ private void sendContentType(final MimeType mimeType) {
+ registerContentTypeRequestChecker(mimeType, HttpMode.GET, ALL_IS_WELL);
+ }
+
+ private void sendContentType(final MimeType mimeType, String checkMessage, String body) throws Exception {
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/incoming_" + mimeType.toString());
+ httpRequest.setConsoleLogResponseBody(true);
+ httpRequest.setContentType(mimeType);
+ if (body != null) {
+ httpRequest.setHttpMode(HttpMode.POST);
+ httpRequest.setRequestBody(body);
+ }
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(checkMessage, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void sendNonAsciiRequestBody() throws Exception {
+ registerContentTypeRequestChecker(MimeType.APPLICATION_JSON, HttpMode.POST, null);
+ sendContentType(MimeType.APPLICATION_JSON, ALL_IS_WELL, ALL_IS_WELL);
+ }
+
+ @Test
+ void sendUTF8RequestBody() throws Exception {
+ Assumptions.assumeFalse(Functions.isWindows(), "TODO does not currently work on Windows");
+ String notAsciiUTF8Message = "ἱερογλύφος";
+ registerContentTypeRequestChecker(MimeType.APPLICATION_JSON_UTF8, HttpMode.POST, null);
+ sendContentType(MimeType.APPLICATION_JSON_UTF8, notAsciiUTF8Message, notAsciiUTF8Message);
+ }
+
+ @Test
+ void sendAllAcceptTypes() throws Exception {
+ for (MimeType mimeType : MimeType.values()) {
+ // Prepare the server
+ registerAcceptedTypeRequestChecker(mimeType);
+ sendAcceptType(mimeType);
+
+ cleanHandlers();
+ }
+ }
+
+ private void sendAcceptType(final MimeType mimeType) throws Exception {
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/accept_" + mimeType.toString());
+ httpRequest.setConsoleLogResponseBody(true);
+ httpRequest.setAcceptType(mimeType);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void canPutResponseInOutputFile() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setOutputFile("file.txt");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+
+ // By default, the response is printed to the console even if an outputFile is used
+ this.j.assertLogContains(ALL_IS_WELL, build);
+
+ // The response is in the output file as well
+ String outputFile = build.getWorkspace().child("file.txt").readToString();
+ Pattern p = Pattern.compile(ALL_IS_WELL);
+ Matcher m = p.matcher(outputFile);
+ assertTrue(m.find());
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void canPutResponseInOutputFileWhenNotSetToGoToConsole() throws Exception {
+ // Prepare the server
+ registerRequestChecker(HttpMode.GET);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/doGET");
+ httpRequest.setOutputFile("file.txt");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+
+ // Check that the console does NOT have the response body
+ this.j.assertLogNotContains(ALL_IS_WELL, build);
+
+ // The response is in the output file
+ String outputFile = build.getWorkspace().child("file.txt").readToString();
+ Pattern p = Pattern.compile(ALL_IS_WELL);
+ Matcher m = p.matcher(outputFile);
+ assertTrue(m.find());
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void timeoutFailsTheBuild() throws Exception {
+ // Prepare the server
+ registerTimeout();
+
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/timeout");
+ httpRequest.setTimeout(2);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Fail: Status code 408 is not in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void canDoCustomHeaders() throws Exception {
+ // Prepare the server
+ registerCustomHeaders();
+
+ List customHeaders = new ArrayList<>();
+ customHeaders.add(new HttpRequestNameValuePair("customHeader", "value1"));
+ customHeaders.add(new HttpRequestNameValuePair("customHeader", "value2"));
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/customHeaders");
+ httpRequest.setCustomHeaders(customHeaders);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void replaceParametersInCustomHeaders() throws Exception {
+ // Prepare the server
+ registerCustomHeadersResolved();
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/customHeadersResolved");
+ httpRequest.setConsoleLogResponseBody(true);
+
+ // Activate requsetBody
+ httpRequest.setHttpMode(HttpMode.POST);
+
+ // Add some custom headers
+ List customHeaders = new ArrayList<>();
+ customHeaders.add(new HttpRequestNameValuePair("resolveCustomParam", "${Tag}"));
+ customHeaders.add(new HttpRequestNameValuePair("resolveEnvParam", "${WORKSPACE}"));
+ httpRequest.setCustomHeaders(customHeaders);
+
+ // Activate passBuildParameters
+ httpRequest.setPassBuildParameters(true);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.addProperty(new ParametersDefinitionProperty(
+ new StringParameterDefinition("Tag", "default"),
+ new StringParameterDefinition("WORKSPACE", "default")
+ ));
+ project.getBuildersList().add(httpRequest);
+
+ FreeStyleBuild build = project.scheduleBuild2(0, new UserIdCause(),
+ new ParametersAction(new StringParameterValue("Tag", "trunk"), new StringParameterValue("WORKSPACE", "C:/path/to/my/workspace"))).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(ALL_IS_WELL, build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void nonExistentBasicAuthFailsTheBuild() throws Exception {
+ // Prepare the server
+ registerBasicAuth();
+
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/basicAuth");
+ httpRequest.setAuthentication("non-existent-key");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Authentication 'non-existent-key' doesn't exist anymore", build);
+ }
+
+ @Test
+ void canDoBasicDigestAuthentication() throws Exception {
+ // Prepare the server
+ registerBasicAuth();
+
+ // Prepare the authentication
+ registerBasicCredential("keyname1", "username1", "password1");
+ registerBasicCredential("keyname2", "username2", "password2");
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/basicAuth");
+ httpRequest.setAuthentication("keyname1");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void testFormAuthentication() throws Exception {
+ final String paramUsername = "username";
+ final String valueUsername = "user";
+ final String paramPassword = "password";
+ final String valuePassword = "pass";
+ final String sessionName = "VALID_SESSIONID";
+
+ registerHandler("/form-auth", HttpMode.POST, new SimpleHandler() {
+ @Override
+ boolean doHandle(Request request, Response response, Callback callback) throws ServletException {
+ Fields parameters;
+ try {
+ parameters = Request.getParameters(request);
+ } catch (Exception e) {
+ throw new ServletException(e);
+ }
+ String username = parameters.getValue(paramUsername);
+ String password = parameters.getValue(paramPassword);
+
+ if (!username.equals(valueUsername) || !password.equals(valuePassword)) {
+ Response.writeError(request, response, callback, HttpStatus.UNAUTHORIZED_401);
+ return true;
+ }
+ HttpCookie cookie = HttpCookie.build(sessionName, "ok").build();
+ Response.addCookie(response, cookie);
+ return okAllIsWell(response, callback);
+ }
+ });
+ registerHandler("/test-auth", HttpMode.GET, new SimpleHandler() {
+ @Override
+ boolean doHandle(Request request, Response response, Callback callback) {
+ String jsessionValue = "";
+ List cookies = Request.getCookies(request);
+ for (HttpCookie cookie : cookies) {
+ if (cookie.getName().equals(sessionName)) {
+ jsessionValue = cookie.getValue();
+ break;
+ }
+ }
+
+ if (!jsessionValue.equals("ok")) {
+ Response.writeError(request, response, callback, HttpStatus.UNAUTHORIZED_401);
+ return true;
+ }
+ return okAllIsWell(response, callback);
+ }
+ });
+
+
+ // Prepare the authentication
+ List params = new ArrayList<>();
+ params.add(new HttpRequestNameValuePair(paramUsername, valueUsername));
+ params.add(new HttpRequestNameValuePair(paramPassword, valuePassword));
+
+ RequestAction action = new RequestAction(new URL(baseURL() + "/form-auth"), HttpMode.POST, null, params);
+ List actions = new ArrayList<>();
+ actions.add(action);
+
+ FormAuthentication formAuth = new FormAuthentication("Form", actions);
+ List formAuthList = new ArrayList<>();
+ formAuthList.add(formAuth);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/test-auth");
+ HttpRequestGlobalConfig.get().setFormAuthentications(formAuthList);
+ httpRequest.setAuthentication("Form");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void canDoFormAuthentication() throws Exception {
+ // Prepare the server
+ registerFormAuth();
+ registerReqAction();
+
+ // Prepare the authentication
+ List params = new ArrayList<>();
+ params.add(new HttpRequestNameValuePair("param1", "value1"));
+ params.add(new HttpRequestNameValuePair("param2", "value2"));
+
+ RequestAction action = new RequestAction(new URL(baseURL() + "/reqAction"), HttpMode.GET, null, params);
+ List actions = new ArrayList<>();
+ actions.add(action);
+
+ FormAuthentication formAuth = new FormAuthentication("keyname", actions);
+ List formAuthList = new ArrayList<>();
+ formAuthList.add(formAuth);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/formAuth");
+ HttpRequestGlobalConfig.get().setFormAuthentications(formAuthList);
+ httpRequest.setAuthentication("keyname");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains("Success: Status code 200 is in the accepted range: 100:399", build);
+ }
+
+ @Test
+ void rejectedFormCredentialsFailTheBuild() throws Exception {
+ // Prepare the server
+ registerFormAuthBad();
+
+ // Prepare the authentication
+ List params = new ArrayList<>();
+ params.add(new HttpRequestNameValuePair("param1", "value1"));
+ params.add(new HttpRequestNameValuePair("param2", "value2"));
+
+ RequestAction action = new RequestAction(new URL(baseURL() + "/formAuthBad"), HttpMode.GET, null, params);
+ List actions = new ArrayList<>();
+ actions.add(action);
+
+ FormAuthentication formAuth = new FormAuthentication("keyname", actions);
+ List formAuthList = new ArrayList<>();
+ formAuthList.add(formAuth);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/formAuthBad");
+ httpRequest.setConsoleLogResponseBody(true);
+ HttpRequestGlobalConfig.get().setFormAuthentications(formAuthList);
+ httpRequest.setAuthentication("keyname");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Error doing authentication", build);
+ }
+
+ @Test
+ void invalidKeyFormAuthenticationFailsTheBuild() throws Exception {
+ // Prepare the server
+
+
+ // Prepare the authentication
+ List params = new ArrayList<>();
+ params.add(new HttpRequestNameValuePair("param1", "value1"));
+ params.add(new HttpRequestNameValuePair("param2", "value2"));
+
+ // The request action won't be sent but we need to prepare it
+ RequestAction action = new RequestAction(new URL(baseURL() + "/non-existent"), HttpMode.GET, null, params);
+ List actions = new ArrayList<>();
+ actions.add(action);
+
+ FormAuthentication formAuth = new FormAuthentication("keyname", actions);
+ List formAuthList = new ArrayList<>();
+ formAuthList.add(formAuth);
+
+ // Prepare HttpRequest - the actual request won't be sent
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/non-existent");
+ httpRequest.setConsoleLogResponseBody(true);
+ HttpRequestGlobalConfig.get().setFormAuthentications(formAuthList);
+
+ // Select a non-existent form authentication, this will error the build before any request is made
+ httpRequest.setAuthentication("non-existent");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Authentication 'non-existent' doesn't exist anymore", build);
+ }
+
+ @Test
+ void responseContentSupplierHeadersFilling() throws Exception {
+ // Prepare test context
+ CloseableHttpResponse response = HttpResponseAdapter.adapt(new BasicClassicHttpResponse(200, "OK"));
+ response.setEntity(new StringEntity("TEST"));
+ response.setHeader("Server", "Jenkins");
+ response.setHeader("Set-Cookie", "JSESSIONID=123456789");
+ response.addHeader("Set-Cookie", "JSESSIONID=abcdefghijk");
+ // Run test
+ ResponseContentSupplier respSupplier = new ResponseContentSupplier(ResponseHandle.STRING, response);
+ // Check expectations
+ assertEquals(2, respSupplier.getHeaders().size());
+ assertTrue(respSupplier.getHeaders().containsKey("Server"));
+ assertTrue(respSupplier.getHeaders().containsKey("Set-Cookie"));
+ assertEquals(1, respSupplier.getHeaders().get("Server").size());
+ assertEquals(2, respSupplier.getHeaders().get("Set-Cookie").size());
+ assertEquals("Jenkins", respSupplier.getHeaders().get("Server").get(0));
+ int valuesFoundCounter = 0;
+ for (String s : respSupplier.getHeaders().get("Set-Cookie")) {
+ if ("JSESSIONID=123456789".equals(s)) {
+ valuesFoundCounter++;
+ } else if ("JSESSIONID=abcdefghijk".equals(s)) {
+ valuesFoundCounter++;
+ }
+ }
+ assertEquals(2, valuesFoundCounter);
+ respSupplier.close();
+ }
+
+ @Test
+ void responseContentSupplierHeadersCaseInsensitivity() throws Exception {
+ // Prepare test context
+ CloseableHttpResponse response = HttpResponseAdapter.adapt(new BasicClassicHttpResponse(200, "OK"));
+ response.setEntity(new StringEntity("TEST"));
+ response.setHeader("Server", "Jenkins");
+ // Run test
+ ResponseContentSupplier respSupplier = new ResponseContentSupplier(ResponseHandle.STRING, response);
+ // Check expectations
+ assertEquals(1, respSupplier.getHeaders().size());
+ assertTrue(respSupplier.getHeaders().containsKey("Server"));
+ assertTrue(respSupplier.getHeaders().containsKey("SERVER"));
+ assertTrue(respSupplier.getHeaders().containsKey("server"));
+ respSupplier.close();
+ }
+
+ @Test
+ void responseContentSupplierHandlesNoContentTypeHeader() throws Exception {
+ // Prepare test context - 204 No Content response without Content-Type header
+ CloseableHttpResponse response = HttpResponseAdapter.adapt(new BasicClassicHttpResponse(204, "No Content"));
+ // Don't set entity or Content-Type header to simulate GitHub API 204 response
+ // Run test
+ ResponseContentSupplier respSupplier = new ResponseContentSupplier(ResponseHandle.NONE, response);
+ // Check expectations - should not throw NullPointerException
+ assertEquals(204, respSupplier.getStatus());
+ respSupplier.close();
+ }
+
+ @Test
+ void testFileUpload() throws Exception {
+ // Prepare the server
+ final File testFolder = newFolder(folder, "junit");
+ File uploadFile = File.createTempFile("upload", ".zip", testFolder);
+ String responseText = "File upload successful!";
+ registerFileUpload(uploadFile, responseText);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/uploadFile");
+ httpRequest.setHttpMode(HttpMode.POST);
+ httpRequest.setValidResponseCodes("201");
+ httpRequest.setConsoleLogResponseBody(true);
+ httpRequest.setUploadFile(uploadFile.getAbsolutePath());
+ httpRequest.setMultipartName("file-name");
+ httpRequest.setContentType(MimeType.APPLICATION_ZIP);
+ httpRequest.setAcceptType(MimeType.TEXT_PLAIN);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(responseText, build);
+ this.j.assertLogContains("Success: Status code 201 is in the accepted range: 201", build);
+ }
+
+ @Test
+ void testUnwrappedPutFileUpload() throws Exception {
+ // Prepare the server
+ final File testFolder = newFolder(folder, "junit");
+ File uploadFile = File.createTempFile("upload", ".zip", testFolder);
+ String responseText = "File upload successful!";
+ registerUnwrappedPutFileUpload(uploadFile, responseText);
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/uploadFile/" + uploadFile.getName());
+ httpRequest.setHttpMode(HttpMode.PUT);
+ httpRequest.setValidResponseCodes("201");
+ httpRequest.setConsoleLogResponseBody(true);
+ httpRequest.setUploadFile(uploadFile.getAbsolutePath());
+ httpRequest.setWrapAsMultipart(false);
+ httpRequest.setContentType(MimeType.APPLICATION_ZIP);
+ httpRequest.setAcceptType(MimeType.TEXT_PLAIN);
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatusSuccess(build);
+ this.j.assertLogContains(responseText, build);
+ this.j.assertLogContains("Success: Status code 201 is in the accepted range: 201", build);
+ }
+
+ @Test
+ void nonExistentProxyAuthFailsTheBuild() throws Exception {
+ // Prepare the server
+ registerBasicAuth();
+
+ // Prepare HttpRequest
+ HttpRequest httpRequest = new HttpRequest(baseURL() + "/basicAuth");
+ httpRequest.setHttpProxy("http://proxy.example.com:8888");
+ httpRequest.setProxyAuthentication("non-existent-key");
+
+ // Run build
+ FreeStyleProject project = this.j.createFreeStyleProject();
+ project.getBuildersList().add(httpRequest);
+ FreeStyleBuild build = project.scheduleBuild2(0).get();
+
+ // Check expectations
+ this.j.assertBuildStatus(Result.FAILURE, build);
+ this.j.assertLogContains("Proxy authentication 'non-existent-key' doesn't exist anymore or is not a username/password credential type", build);
+ }
+
+ private static File newFolder(File root, String... subDirs) throws IOException {
+ String subFolder = String.join("/", subDirs);
+ File result = new File(root, subFolder);
+ if (!result.mkdirs()) {
+ throw new IOException("Couldn't create folders " + root);
+ }
+ return result;
+ }
+
+ private static class RedirectStatus {
+ boolean called = false;
+ boolean redirected = false;
+ };
+
+ @Test
+ void noRedirectOnPOSTOn302() throws Exception {
+ final RedirectStatus redirectStatus = new RedirectStatus();
+
+ registerHandler("/redirected", HttpMode.GET, new SimpleHandler() {
+ @Override
+ boolean doHandle(Request request, Response response, Callback callback) {
+ redirectStatus.called = true;
+ return okAllIsWell(response, callback);
+ }
+ });
+ registerHandler("/redirectPOST", HttpMode.POST, new SimpleHandler() {
+ @Override
+ boolean doHandle(Request request, Response response, Callback callback) {
+ redirectStatus.redirected = true;
+ Response.sendRedirect(request, response, callback, 302, "/redirected", false);
+ return true;
+ }
+ });
+ String body = "send-body-workflow";
+ WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "postBody");
+ proj.setDefinition(new CpsFlowDefinition(
+ "def response = httpRequest" +
+ " httpMode: 'POST'," +
+ " requestBody: '" + body + "'," +
+ " url: '" + baseURL() + "/redirectPOST'\n" +
+ "println('Response: ' + response.content)\n",
+ true));
+
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+
+ // Check expectations
+ j.assertBuildStatusSuccess(run);
+ assertTrue(redirectStatus.redirected);
+ assertFalse(redirectStatus.called);
+ }
+
+ @Test
+ void redirectOnGETOn302() throws Exception {
+ final RedirectStatus redirectStatus = new RedirectStatus();
+
+ registerHandler("/redirected", HttpMode.GET, new SimpleHandler() {
+ @Override
+ boolean doHandle(Request request, Response response, Callback callback) {
+ redirectStatus.called = true;
+ return okAllIsWell(response, callback);
+ }
+ });
+ registerHandler("/redirectGET", HttpMode.GET, new SimpleHandler() {
+ @Override
+ boolean doHandle(Request request, Response response, Callback callback) {
+ redirectStatus.redirected = true;
+ Response.sendRedirect(request, response, callback, 302, "/redirected", false);
+ return true;
+ }
+ });
+ String body = "send-body-workflow";
+ WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "postBody");
+ proj.setDefinition(new CpsFlowDefinition(
+ "def response = httpRequest" +
+ " httpMode: 'GET'," +
+ " requestBody: '" + body + "'," +
+ " url: '" + baseURL() + "/redirectGET'\n" +
+ "println('Response: ' + response.content)\n",
+ true));
+
+ WorkflowRun run = proj.scheduleBuild2(0).get();
+
+ // Check expectations
+ j.assertBuildStatusSuccess(run);
+ assertTrue(redirectStatus.redirected);
+ assertTrue(redirectStatus.called);
+ }
+}
diff --git a/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherTestBase.java b/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherTestBase.java
new file mode 100644
index 00000000..cfe0cb8d
--- /dev/null
+++ b/src/test/java/jenkins/plugins/http_request/HttpRequestPublisherTestBase.java
@@ -0,0 +1,11 @@
+package jenkins.plugins.http_request;
+
+/**
+ * Shares the {@link HttpRequestTestBase} echo server fixture (and its {@code SERVER} instance that
+ * {@link Registers} registers handlers against) so publisher tests can drive the same harness as the
+ * build-step and pipeline-step tests.
+ *
+ * @author Martin d'Anjou
+ */
+class HttpRequestPublisherTestBase extends HttpRequestTestBase {
+}
diff --git a/src/test/resources/testdata/test-get-response.json b/src/test/resources/testdata/test-get-response.json
new file mode 100644
index 00000000..80e80979
--- /dev/null
+++ b/src/test/resources/testdata/test-get-response.json
@@ -0,0 +1,10 @@
+{
+ "status": "ok",
+ "message": "GET call against the Jenkins Test Harness succeeded.",
+ "timestamp": "2026-07-01T00:00:00Z",
+ "items": [
+ { "id": 1, "name": "alpha" },
+ { "id": 2, "name": "beta" },
+ { "id": 3, "name": "gamma" }
+ ]
+}
\ No newline at end of file
diff --git a/src/test/resources/testdata/test-post-response.json b/src/test/resources/testdata/test-post-response.json
new file mode 100644
index 00000000..3eef5a18
--- /dev/null
+++ b/src/test/resources/testdata/test-post-response.json
@@ -0,0 +1,10 @@
+{
+ "status": "ok",
+ "message": "POST call against the Jenkins Test Harness succeeded.",
+ "timestamp": "2026-07-01T00:00:00Z",
+ "items": [
+ { "id": 1, "name": "alpha" },
+ { "id": 2, "name": "beta" },
+ { "id": 3, "name": "gamma" }
+ ]
+}
\ No newline at end of file