From c100c21d4ba03dbb6c8376118601f17fd6bd46f6 Mon Sep 17 00:00:00 2001
From: Vishal Wagh <169045855+vwagh-dev@users.noreply.github.com>
Date: Tue, 1 Jul 2025 01:35:13 +0530
Subject: [PATCH] integration test for jenkins-75676
---
pom.xml | 36 ++++
.../java/integration/JENKINS_75676_Test.java | 191 ++++++++++++++++++
.../JENKINS-75676/generate-mtls-certs.sh | 82 ++++++++
.../resources/JENKINS-75676/nginx.conf.tmpl | 21 ++
4 files changed, 330 insertions(+)
create mode 100644 src/test/java/integration/JENKINS_75676_Test.java
create mode 100755 src/test/resources/JENKINS-75676/generate-mtls-certs.sh
create mode 100644 src/test/resources/JENKINS-75676/nginx.conf.tmpl
diff --git a/pom.xml b/pom.xml
index a974aa0e9..691e31ff1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -186,6 +186,42 @@
+
+ org.testcontainers
+ testcontainers
+ 1.19.7
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ 1.19.7
+ test
+
+
+ org.testcontainers
+ nginx
+ 1.19.7
+ test
+
+
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ 4.12.0
+ test
+
diff --git a/src/test/java/integration/JENKINS_75676_Test.java b/src/test/java/integration/JENKINS_75676_Test.java
new file mode 100644
index 000000000..2567dace0
--- /dev/null
+++ b/src/test/java/integration/JENKINS_75676_Test.java
@@ -0,0 +1,191 @@
+package integration;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.testcontainers.containers.NginxContainer;
+import org.testcontainers.utility.MountableFile;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.CertificateFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class JENKINS_75676_Test {
+ static MockWebServer bitbucketMock;
+ static NginxContainer> nginx;
+ static File tmpConf;
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @BeforeClass
+ public static void setup() throws IOException, InterruptedException {
+ runScript("src/test/resources/JENKINS-75676/generate-mtls-certs.sh");
+ bitbucketMock = new MockWebServer();
+ bitbucketMock.enqueue(new MockResponse()
+ .setBody("Bitbucket Mock Server Up")
+ .setResponseCode(200));
+ bitbucketMock.start();
+
+ // 2. Dynamically write nginx.conf, using the mockwebserver port and host.docker.internal
+ int bbPort = bitbucketMock.getPort();
+ String nginxConf = renderNginxConfTemplate(bbPort);
+
+ tmpConf = File.createTempFile("nginx-ssl", ".conf");
+ Files.write(tmpConf.toPath(), nginxConf.getBytes(StandardCharsets.UTF_8));
+
+ // 3. Start Nginx container, using the temp conf
+ nginx = new NginxContainer<>("nginx:1.28")
+ .withExposedPorts(8443)
+ .withCopyFileToContainer(MountableFile.forHostPath(tmpConf.getAbsolutePath()), "/etc/nginx/nginx.conf")
+ .withCopyFileToContainer(MountableFile.forClasspathResource("JENKINS-75676/certs/nginx/server.crt"), "/etc/nginx/certs/server.crt")
+ .withCopyFileToContainer(MountableFile.forClasspathResource("JENKINS-75676/certs/nginx/server.key"), "/etc/nginx/certs/server.key")
+ .withCopyFileToContainer(MountableFile.forClasspathResource("JENKINS-75676/certs/nginx/rootCA.crt"), "/etc/nginx/certs/rootCA.crt");
+ nginx.start();
+ }
+ @AfterClass
+ public static void tearDown() throws IOException {
+ // 5. Cleanup
+ if (nginx != null)
+ nginx.stop();
+ if(bitbucketMock != null)
+ bitbucketMock.shutdown();
+ if (tmpConf != null && tmpConf.exists())
+ tmpConf.delete();
+ }
+ @Ignore
+ @Test
+ public void canAccessViaNginxProxy() throws Exception {
+ String proxyUrl = "http://" + nginx.getHost() + ":" + nginx.getMappedPort(8443) + "/";
+ HttpURLConnection conn = (HttpURLConnection) new URL(proxyUrl).openConnection();
+ int code = conn.getResponseCode();
+ String body = new java.util.Scanner(conn.getInputStream()).useDelimiter("\\A").next();
+ assertThat(code).isEqualTo(200);
+ assertThat(body).contains("Bitbucket Mock Server Up");
+
+
+ }
+
+ @Ignore
+ @Test
+ public void proxyReturnsMasterBranch() throws Exception {
+ // Enqueue response for the branches endpoint
+ bitbucketMock.enqueue(new MockResponse()
+ .setResponseCode(200)
+ .setHeader("Content-Type", "application/json")
+ .setBody("{ \"values\": [ { \"displayId\": \"master\", \"id\": \"refs/heads/master\", \"isDefault\": true } ], \"isLastPage\": true }"));
+
+ // Create the proxy URL
+ String branchesUrl = "http://" + nginx.getHost() + ":" + nginx.getMappedPort(8080) +
+ "/rest/api/1.0/projects/myorg/repos/testrepo/branches";
+
+ HttpURLConnection conn = (HttpURLConnection) new URL(branchesUrl).openConnection();
+ conn.setRequestMethod("GET");
+ conn.setRequestProperty("Accept", "application/json");
+ int responseCode = conn.getResponseCode();
+ assertThat(responseCode).isEqualTo(200);
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ StringBuilder body = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ body.append(line);
+ }
+ reader.close();
+
+ String bodyString = body.toString().replaceAll("\\s+", "");
+ assertThat(bodyString).contains("\"displayId\":\"master\"");
+ assertThat(bodyString).contains("\"isDefault\":true");
+ }
+
+
+ @Test
+ public void canAccessViaNginxProxyWithClientCert() throws Exception {
+ SSLContext sslContext = setupSSLContext();
+ String proxyUrl = "https://" + nginx.getHost() + ":" + nginx.getMappedPort(8443) + "/";
+ HttpsURLConnection conn = (HttpsURLConnection) new URL(proxyUrl).openConnection();
+ conn.setSSLSocketFactory(sslContext.getSocketFactory());
+ int code = conn.getResponseCode();
+ String body = new java.util.Scanner(conn.getInputStream()).useDelimiter("\\A").next();
+ assertThat(code).isEqualTo(200);
+ assertThat(body).contains("Bitbucket Mock Server Up");
+ }
+
+ public static SSLContext setupSSLContext() throws Exception {
+ // Load PKCS12 keystore (from cert generation script)
+ char[] password = "changeit".toCharArray();
+ KeyStore clientStore = KeyStore.getInstance("PKCS12");
+ try (InputStream in = JENKINS_75676_Test.class.getResourceAsStream("/JENKINS-75676/certs/client_keystore.p12")) {
+ clientStore.load(in, password);
+ }
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(clientStore, password);
+
+ // Trust store with CA root
+ KeyStore trustStore = KeyStore.getInstance("JKS");
+ trustStore.load(null, null);
+ try (InputStream in = JENKINS_75676_Test.class.getResourceAsStream("/JENKINS-75676/certs/rootCA.crt")) {
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ trustStore.setCertificateEntry("ca", cf.generateCertificate(in));
+ }
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(trustStore);
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
+ return sslContext;
+ }
+
+ private static String renderNginxConfTemplate(int bitbucketPort) throws IOException {
+ try (InputStream is = JENKINS_75676_Test.class.getResourceAsStream("/JENKINS-75676/nginx.conf.tmpl")) {
+ if (is == null)
+ throw new IllegalStateException("Missing nginx.conf.tmpl file in test resources");
+ String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ return content.replace("${BITBUCKET_PORT}", String.valueOf(bitbucketPort));
+ }
+ }
+
+ private static void runScript(String scriptPath) throws IOException, InterruptedException {
+ File script = new File(scriptPath);
+ if (!script.exists()) {
+ throw new IllegalArgumentException("Cert generation script does not exist: " + scriptPath);
+ }
+ // Make script executable if not already
+ script.setExecutable(true);
+
+ // Run the script
+ ProcessBuilder pb = new ProcessBuilder("bash", script.getAbsolutePath());
+ pb.redirectErrorStream(true); // merge stderr with stdout
+ Process process = pb.start();
+ try (BufferedReader r = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = r.readLine()) != null) {
+ System.out.println("[certgen] " + line);
+ }
+ }
+ int exitCode = process.waitFor();
+ if (exitCode != 0) {
+ throw new IllegalStateException("Cert generation script failed with exit code " + exitCode);
+ }
+ }
+
+}
diff --git a/src/test/resources/JENKINS-75676/generate-mtls-certs.sh b/src/test/resources/JENKINS-75676/generate-mtls-certs.sh
new file mode 100755
index 000000000..fb3ab6044
--- /dev/null
+++ b/src/test/resources/JENKINS-75676/generate-mtls-certs.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+set -euo pipefail
+
+BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+CERT_DIR="$BASE_DIR/certs"
+rm -rf "$CERT_DIR"
+mkdir -p "$CERT_DIR"
+cd "$CERT_DIR"
+
+echo "1. Generate Root CA"
+openssl genrsa -out rootCA.key 4096
+openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1825 -out rootCA.crt \
+ -subj "/C=ES/L=Sevilla/O=TestOrg/CN=TestRootCA"
+
+echo "2. Create Server Certificate (localhost, 127.0.0.1 SAN)"
+cat > server_cert.cnf < client_cert.cnf <