diff --git a/README.md b/README.md
index 08e9ad86580..cdedf92ee68 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@
* [Thread safety considerations](#thread-safety-considerations)
* [Spring Support](#spring-support)
* [Configuration reference](#configuration-reference)
+* [WireMock Integration Testing](#wiremock-integration-testing)
* [Building the SDK](#building-the-sdk)
* [Contributing](#contributing)
@@ -803,8 +804,21 @@ ApiClient client = Clients.builder()
```
[//]: # (end: disableCaching)
+## WireMock Integration Testing
+
+WireMock can be configured to serve HTTPS with a self-signed certificate and custom KeyStore. The SDK's HTTP client can be configured with a custom SSLContext and TrustManager to accept the certificate. This implementation demonstrates both: automatic self-signed certificate generation, WireMock HTTPS configuration, and SDK HTTP client setup with a custom TrustManager. It also uses dynamic port allocation for thread-safe parallel test execution.
+
+### Running the Tests
+
+```bash
+mvn test -Dtest=WireMockOktaClientTest -pl integration-tests
+```
+
+See the complete implementation in `integration-tests/src/test/java/com/okta/sdk/tests/WireMockOktaClientTest.java`.
+
## Building the SDK
+
In most cases, you won't need to build the SDK from source. If you want to build it yourself, take a look at the [build instructions wiki](https://github.com/okta/okta-sdk-java/wiki/Build-It) (though just cloning the repo and running `mvn install` should get you going).
> **Note**: The SDK uses a large OpenAPI specification file (~84,000 lines). If you encounter memory issues during build:
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index e18e0bf9791..8b5bd0a7de2 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -78,8 +78,8 @@
com.github.tomakehurst
- wiremock-standalone
- 2.27.2
+ wiremock-jre8
+ 2.35.0
test
diff --git a/integration-tests/src/test/java/com/okta/sdk/tests/WireMockOktaClientTest.java b/integration-tests/src/test/java/com/okta/sdk/tests/WireMockOktaClientTest.java
new file mode 100644
index 00000000000..49dcc38cd86
--- /dev/null
+++ b/integration-tests/src/test/java/com/okta/sdk/tests/WireMockOktaClientTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2017 Okta
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.okta.sdk.tests;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.okta.sdk.client.Clients;
+import com.okta.sdk.resource.api.UserApi;
+import com.okta.sdk.resource.client.ApiClient;
+import com.okta.sdk.resource.client.ApiException;
+import com.okta.sdk.resource.model.User;
+import com.okta.sdk.resource.model.UserProfile;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.FileInputStream;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.util.List;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertEquals;
+
+import java.net.ServerSocket;
+
+/**
+ * Integration test demonstrating WireMock + Okta SDK with HTTPS using self-signed certificates.
+ * This test proves the solution works end-to-end without hitting actual Okta servers.
+ *
+ * Thread-safe design: Uses dynamic port allocation for each test instance,
+ * allowing parallel test execution without port conflicts.
+ */
+public class WireMockOktaClientTest {
+
+ private WireMockServer wireMockServer;
+ private ApiClient client;
+ private UserApi userApi;
+ private int wireMockHttpsPort; // Dynamic port for thread-safety
+ private String wireMockHost; // Computed from dynamic port
+ private static final String KEYSTORE_PATH = "../../wiremock-keystore.jks"; // Path from integration-tests module
+ private static final String KEYSTORE_PASSWORD = "password";
+ private static final Object KEYSTORE_LOCK = new Object(); // Lock for thread-safe keystore generation
+
+ @BeforeMethod
+ public void setup() throws Exception {
+ // Generate WireMock keystore if it doesn't exist (synchronized for thread-safety)
+ String keystorePath = Paths.get(KEYSTORE_PATH).toAbsolutePath().toString();
+ synchronized(KEYSTORE_LOCK) {
+ java.io.File keystoreFile = new java.io.File(keystorePath);
+ if (!keystoreFile.exists()) {
+ System.out.println("[Thread: " + Thread.currentThread().getName() + "] " +
+ "Generating WireMock keystore at: " + keystorePath);
+ ProcessBuilder pb = new ProcessBuilder(
+ "keytool", "-genkey", "-alias", "wiremock", "-keyalg", "RSA",
+ "-keystore", keystorePath,
+ "-storepass", KEYSTORE_PASSWORD, "-keypass", KEYSTORE_PASSWORD,
+ "-dname", "CN=localhost", "-validity", "365", "-noprompt"
+ );
+ int exitCode = pb.start().waitFor();
+ if (exitCode != 0) {
+ throw new RuntimeException("Failed to generate WireMock keystore. " +
+ "Ensure 'keytool' is in your PATH (comes with Java)");
+ }
+ System.out.println("[Thread: " + Thread.currentThread().getName() + "] " +
+ "WireMock keystore generated successfully");
+ }
+ }
+
+ // Allocate a dynamic HTTPS port for this test instance (thread-safe)
+ wireMockHttpsPort = allocateAvailablePort();
+ wireMockHost = "https://localhost:" + wireMockHttpsPort;
+ System.out.println("[Thread: " + Thread.currentThread().getName() + "] " +
+ "Using dynamic HTTPS port: " + wireMockHttpsPort);
+
+ // Start WireMock on dynamic HTTPS port with self-signed certificate
+ wireMockServer = new WireMockServer(
+ WireMockConfiguration.wireMockConfig()
+ .httpsPort(wireMockHttpsPort)
+ .keystorePath(KEYSTORE_PATH)
+ .keystorePassword(KEYSTORE_PASSWORD)
+ );
+ wireMockServer.start();
+
+ // Configure custom SSL context with the self-signed keystore
+ KeyStore trustStore = KeyStore.getInstance("JKS");
+ try (FileInputStream fis = new FileInputStream(keystorePath)) {
+ trustStore.load(fis, KEYSTORE_PASSWORD.toCharArray());
+ }
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(trustStore);
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, tmf.getTrustManagers(), new java.security.SecureRandom());
+
+ // Build HttpClient with custom SSL context using HTTP Client 5 APIs
+ // We need to set up the connection manager with custom SSL context
+ org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient =
+ HttpClients.custom()
+ .setConnectionManager(
+ PoolingHttpClientConnectionManagerBuilder.create()
+ .setSSLSocketFactory(
+ new org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory(sslContext)
+ )
+ .build()
+ )
+ .build();
+
+ // Create ApiClient with the custom HttpClient and a disabled cache manager
+ client = new ApiClient(httpClient, new com.okta.sdk.impl.cache.DisabledCacheManager());
+ client.setBasePath(wireMockHost); // Use dynamic host with dynamic port
+
+ userApi = new UserApi(client);
+ }
+
+ /**
+ * Allocates an available port by binding to port 0 (OS assigns available port).
+ * This ensures thread-safe, collision-free port allocation.
+ *
+ * @return an available port number
+ * @throws Exception if port allocation fails
+ */
+ private int allocateAvailablePort() throws Exception {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ return socket.getLocalPort();
+ }
+ }
+
+ @AfterMethod
+ public void teardown() {
+ if (wireMockServer != null) {
+ wireMockServer.stop();
+ }
+ }
+
+ @Test
+ public void testGetUser() throws ApiException {
+ // Mock the Okta API endpoint for getting a user
+ String userId = "00ub0oNGTSWTBKOLGLHN";
+ stubFor(get(urlEqualTo("/api/v1/users/" + userId))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{" +
+ "\"id\":\"" + userId + "\"," +
+ "\"status\":\"ACTIVE\"," +
+ "\"created\":\"2013-06-24T16:39:18.000Z\"," +
+ "\"activated\":\"2013-06-24T16:39:19.000Z\"," +
+ "\"statusChanged\":\"2013-06-24T16:39:19.000Z\"," +
+ "\"lastLogin\":\"2013-10-02T14:06:25.000Z\"," +
+ "\"lastUpdated\":\"2013-10-02T14:06:25.000Z\"," +
+ "\"passwordChanged\":\"2013-09-11T23:30:26.000Z\"," +
+ "\"profile\":{" +
+ "\"firstName\":\"Isaac\"," +
+ "\"lastName\":\"Brock\"," +
+ "\"email\":\"isaac.brock@example.com\"," +
+ "\"login\":\"isaac.brock@example.com\"," +
+ "\"mobilePhone\":null" +
+ "}" +
+ "}")
+ ));
+
+ // Call the SDK to get the user
+ User user = userApi.getUser(userId, null, null);
+
+ // Verify the response
+ assertNotNull(user);
+ assertEquals(userId, user.getId());
+ assertEquals("ACTIVE", user.getStatus().toString());
+ assertNotNull(user.getProfile());
+ assertEquals("isaac.brock@example.com", user.getProfile().getEmail());
+ assertEquals("Isaac", user.getProfile().getFirstName());
+ assertEquals("Brock", user.getProfile().getLastName());
+ }
+
+ @Test
+ public void testListUsers() throws ApiException {
+ // Mock the Okta API endpoint for listing users
+ stubFor(get(urlEqualTo("/api/v1/users"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("[" +
+ "{" +
+ "\"id\":\"00ub0oNGTSWTBKOLGLHN\"," +
+ "\"status\":\"ACTIVE\"," +
+ "\"created\":\"2013-06-24T16:39:18.000Z\"," +
+ "\"activated\":\"2013-06-24T16:39:19.000Z\"," +
+ "\"statusChanged\":\"2013-06-24T16:39:19.000Z\"," +
+ "\"lastLogin\":\"2013-10-02T14:06:25.000Z\"," +
+ "\"lastUpdated\":\"2013-10-02T14:06:25.000Z\"," +
+ "\"passwordChanged\":\"2013-09-11T23:30:26.000Z\"," +
+ "\"profile\":{" +
+ "\"firstName\":\"Isaac\"," +
+ "\"lastName\":\"Brock\"," +
+ "\"email\":\"isaac.brock@example.com\"," +
+ "\"login\":\"isaac.brock@example.com\"," +
+ "\"mobilePhone\":null" +
+ "}" +
+ "}," +
+ "{" +
+ "\"id\":\"00ub0oNGTSWTBKOLGLHO\"," +
+ "\"status\":\"ACTIVE\"," +
+ "\"created\":\"2013-06-24T16:39:18.000Z\"," +
+ "\"activated\":\"2013-06-24T16:39:19.000Z\"," +
+ "\"statusChanged\":\"2013-06-24T16:39:19.000Z\"," +
+ "\"lastLogin\":\"2013-10-02T14:06:25.000Z\"," +
+ "\"lastUpdated\":\"2013-10-02T14:06:25.000Z\"," +
+ "\"passwordChanged\":\"2013-09-11T23:30:26.000Z\"," +
+ "\"profile\":{" +
+ "\"firstName\":\"Jane\"," +
+ "\"lastName\":\"Developer\"," +
+ "\"email\":\"jane.developer@example.com\"," +
+ "\"login\":\"jane.developer@example.com\"," +
+ "\"mobilePhone\":null" +
+ "}" +
+ "}" +
+ "]")
+ ));
+
+ // Call the SDK to list users
+ List users = userApi.listUsers(null, null, null, null, null, null, null, null, null, null);
+
+ // Verify the response
+ assertNotNull(users);
+ assertEquals(2, users.size());
+
+ User firstUser = users.get(0);
+ assertEquals("00ub0oNGTSWTBKOLGLHN", firstUser.getId());
+ assertEquals("isaac.brock@example.com", firstUser.getProfile().getEmail());
+
+ User secondUser = users.get(1);
+ assertEquals("00ub0oNGTSWTBKOLGLHO", secondUser.getId());
+ assertEquals("jane.developer@example.com", secondUser.getProfile().getEmail());
+ }
+
+ @Test
+ public void testWireMockHttps() {
+ // This test simply verifies that the WireMock server is running on HTTPS
+ // and the SSL context is properly configured
+ assertNotNull(wireMockServer);
+ assertNotNull(client);
+ assertNotNull(userApi);
+ }
+}
+