Skip to content

Commit 9e9e85c

Browse files
Merge pull request #1665 from okta/TestAgraja
Added wiremock integration guide code and documentation
2 parents 547f0ff + f4683aa commit 9e9e85c

3 files changed

Lines changed: 281 additions & 2 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* [Thread safety considerations](#thread-safety-considerations)
1414
* [Spring Support](#spring-support)
1515
* [Configuration reference](#configuration-reference)
16+
* [WireMock Integration Testing](#wiremock-integration-testing)
1617
* [Building the SDK](#building-the-sdk)
1718
* [Contributing](#contributing)
1819

@@ -803,8 +804,21 @@ ApiClient client = Clients.builder()
803804
```
804805
[//]: # (end: disableCaching)
805806

807+
## WireMock Integration Testing
808+
809+
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.
810+
811+
### Running the Tests
812+
813+
```bash
814+
mvn test -Dtest=WireMockOktaClientTest -pl integration-tests
815+
```
816+
817+
See the complete implementation in `integration-tests/src/test/java/com/okta/sdk/tests/WireMockOktaClientTest.java`.
818+
806819
## Building the SDK
807820

821+
808822
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).
809823

810824
> **Note**: The SDK uses a large OpenAPI specification file (~84,000 lines). If you encounter memory issues during build:

integration-tests/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@
7878
</dependency>
7979
<dependency>
8080
<groupId>com.github.tomakehurst</groupId>
81-
<artifactId>wiremock-standalone</artifactId>
82-
<version>2.27.2</version>
81+
<artifactId>wiremock-jre8</artifactId>
82+
<version>2.35.0</version>
8383
<scope>test</scope>
8484
</dependency>
8585
<dependency>
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
* Copyright 2017 Okta
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.okta.sdk.tests;
17+
18+
import com.github.tomakehurst.wiremock.WireMockServer;
19+
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
20+
import com.okta.sdk.client.Clients;
21+
import com.okta.sdk.resource.api.UserApi;
22+
import com.okta.sdk.resource.client.ApiClient;
23+
import com.okta.sdk.resource.client.ApiException;
24+
import com.okta.sdk.resource.model.User;
25+
import com.okta.sdk.resource.model.UserProfile;
26+
import org.apache.hc.client5.http.impl.classic.HttpClients;
27+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
28+
import org.testng.annotations.AfterMethod;
29+
import org.testng.annotations.BeforeMethod;
30+
import org.testng.annotations.Test;
31+
32+
import javax.net.ssl.SSLContext;
33+
import javax.net.ssl.TrustManagerFactory;
34+
import java.io.FileInputStream;
35+
import java.nio.file.Paths;
36+
import java.security.KeyStore;
37+
import java.util.List;
38+
39+
import static com.github.tomakehurst.wiremock.client.WireMock.*;
40+
import static org.testng.Assert.assertNotNull;
41+
import static org.testng.Assert.assertEquals;
42+
43+
import java.net.ServerSocket;
44+
45+
/**
46+
* Integration test demonstrating WireMock + Okta SDK with HTTPS using self-signed certificates.
47+
* This test proves the solution works end-to-end without hitting actual Okta servers.
48+
*
49+
* Thread-safe design: Uses dynamic port allocation for each test instance,
50+
* allowing parallel test execution without port conflicts.
51+
*/
52+
public class WireMockOktaClientTest {
53+
54+
private WireMockServer wireMockServer;
55+
private ApiClient client;
56+
private UserApi userApi;
57+
private int wireMockHttpsPort; // Dynamic port for thread-safety
58+
private String wireMockHost; // Computed from dynamic port
59+
private static final String KEYSTORE_PATH = "../../wiremock-keystore.jks"; // Path from integration-tests module
60+
private static final String KEYSTORE_PASSWORD = "password";
61+
private static final Object KEYSTORE_LOCK = new Object(); // Lock for thread-safe keystore generation
62+
63+
@BeforeMethod
64+
public void setup() throws Exception {
65+
// Generate WireMock keystore if it doesn't exist (synchronized for thread-safety)
66+
String keystorePath = Paths.get(KEYSTORE_PATH).toAbsolutePath().toString();
67+
synchronized(KEYSTORE_LOCK) {
68+
java.io.File keystoreFile = new java.io.File(keystorePath);
69+
if (!keystoreFile.exists()) {
70+
System.out.println("[Thread: " + Thread.currentThread().getName() + "] " +
71+
"Generating WireMock keystore at: " + keystorePath);
72+
ProcessBuilder pb = new ProcessBuilder(
73+
"keytool", "-genkey", "-alias", "wiremock", "-keyalg", "RSA",
74+
"-keystore", keystorePath,
75+
"-storepass", KEYSTORE_PASSWORD, "-keypass", KEYSTORE_PASSWORD,
76+
"-dname", "CN=localhost", "-validity", "365", "-noprompt"
77+
);
78+
int exitCode = pb.start().waitFor();
79+
if (exitCode != 0) {
80+
throw new RuntimeException("Failed to generate WireMock keystore. " +
81+
"Ensure 'keytool' is in your PATH (comes with Java)");
82+
}
83+
System.out.println("[Thread: " + Thread.currentThread().getName() + "] " +
84+
"WireMock keystore generated successfully");
85+
}
86+
}
87+
88+
// Allocate a dynamic HTTPS port for this test instance (thread-safe)
89+
wireMockHttpsPort = allocateAvailablePort();
90+
wireMockHost = "https://localhost:" + wireMockHttpsPort;
91+
System.out.println("[Thread: " + Thread.currentThread().getName() + "] " +
92+
"Using dynamic HTTPS port: " + wireMockHttpsPort);
93+
94+
// Start WireMock on dynamic HTTPS port with self-signed certificate
95+
wireMockServer = new WireMockServer(
96+
WireMockConfiguration.wireMockConfig()
97+
.httpsPort(wireMockHttpsPort)
98+
.keystorePath(KEYSTORE_PATH)
99+
.keystorePassword(KEYSTORE_PASSWORD)
100+
);
101+
wireMockServer.start();
102+
103+
// Configure custom SSL context with the self-signed keystore
104+
KeyStore trustStore = KeyStore.getInstance("JKS");
105+
try (FileInputStream fis = new FileInputStream(keystorePath)) {
106+
trustStore.load(fis, KEYSTORE_PASSWORD.toCharArray());
107+
}
108+
109+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
110+
tmf.init(trustStore);
111+
112+
SSLContext sslContext = SSLContext.getInstance("TLS");
113+
sslContext.init(null, tmf.getTrustManagers(), new java.security.SecureRandom());
114+
115+
// Build HttpClient with custom SSL context using HTTP Client 5 APIs
116+
// We need to set up the connection manager with custom SSL context
117+
org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient =
118+
HttpClients.custom()
119+
.setConnectionManager(
120+
PoolingHttpClientConnectionManagerBuilder.create()
121+
.setSSLSocketFactory(
122+
new org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory(sslContext)
123+
)
124+
.build()
125+
)
126+
.build();
127+
128+
// Create ApiClient with the custom HttpClient and a disabled cache manager
129+
client = new ApiClient(httpClient, new com.okta.sdk.impl.cache.DisabledCacheManager());
130+
client.setBasePath(wireMockHost); // Use dynamic host with dynamic port
131+
132+
userApi = new UserApi(client);
133+
}
134+
135+
/**
136+
* Allocates an available port by binding to port 0 (OS assigns available port).
137+
* This ensures thread-safe, collision-free port allocation.
138+
*
139+
* @return an available port number
140+
* @throws Exception if port allocation fails
141+
*/
142+
private int allocateAvailablePort() throws Exception {
143+
try (ServerSocket socket = new ServerSocket(0)) {
144+
return socket.getLocalPort();
145+
}
146+
}
147+
148+
@AfterMethod
149+
public void teardown() {
150+
if (wireMockServer != null) {
151+
wireMockServer.stop();
152+
}
153+
}
154+
155+
@Test
156+
public void testGetUser() throws ApiException {
157+
// Mock the Okta API endpoint for getting a user
158+
String userId = "00ub0oNGTSWTBKOLGLHN";
159+
stubFor(get(urlEqualTo("/api/v1/users/" + userId))
160+
.willReturn(aResponse()
161+
.withStatus(200)
162+
.withHeader("Content-Type", "application/json")
163+
.withBody("{" +
164+
"\"id\":\"" + userId + "\"," +
165+
"\"status\":\"ACTIVE\"," +
166+
"\"created\":\"2013-06-24T16:39:18.000Z\"," +
167+
"\"activated\":\"2013-06-24T16:39:19.000Z\"," +
168+
"\"statusChanged\":\"2013-06-24T16:39:19.000Z\"," +
169+
"\"lastLogin\":\"2013-10-02T14:06:25.000Z\"," +
170+
"\"lastUpdated\":\"2013-10-02T14:06:25.000Z\"," +
171+
"\"passwordChanged\":\"2013-09-11T23:30:26.000Z\"," +
172+
"\"profile\":{" +
173+
"\"firstName\":\"Isaac\"," +
174+
"\"lastName\":\"Brock\"," +
175+
"\"email\":\"isaac.brock@example.com\"," +
176+
"\"login\":\"isaac.brock@example.com\"," +
177+
"\"mobilePhone\":null" +
178+
"}" +
179+
"}")
180+
));
181+
182+
// Call the SDK to get the user
183+
User user = userApi.getUser(userId, null, null);
184+
185+
// Verify the response
186+
assertNotNull(user);
187+
assertEquals(userId, user.getId());
188+
assertEquals("ACTIVE", user.getStatus().toString());
189+
assertNotNull(user.getProfile());
190+
assertEquals("isaac.brock@example.com", user.getProfile().getEmail());
191+
assertEquals("Isaac", user.getProfile().getFirstName());
192+
assertEquals("Brock", user.getProfile().getLastName());
193+
}
194+
195+
@Test
196+
public void testListUsers() throws ApiException {
197+
// Mock the Okta API endpoint for listing users
198+
stubFor(get(urlEqualTo("/api/v1/users"))
199+
.willReturn(aResponse()
200+
.withStatus(200)
201+
.withHeader("Content-Type", "application/json")
202+
.withBody("[" +
203+
"{" +
204+
"\"id\":\"00ub0oNGTSWTBKOLGLHN\"," +
205+
"\"status\":\"ACTIVE\"," +
206+
"\"created\":\"2013-06-24T16:39:18.000Z\"," +
207+
"\"activated\":\"2013-06-24T16:39:19.000Z\"," +
208+
"\"statusChanged\":\"2013-06-24T16:39:19.000Z\"," +
209+
"\"lastLogin\":\"2013-10-02T14:06:25.000Z\"," +
210+
"\"lastUpdated\":\"2013-10-02T14:06:25.000Z\"," +
211+
"\"passwordChanged\":\"2013-09-11T23:30:26.000Z\"," +
212+
"\"profile\":{" +
213+
"\"firstName\":\"Isaac\"," +
214+
"\"lastName\":\"Brock\"," +
215+
"\"email\":\"isaac.brock@example.com\"," +
216+
"\"login\":\"isaac.brock@example.com\"," +
217+
"\"mobilePhone\":null" +
218+
"}" +
219+
"}," +
220+
"{" +
221+
"\"id\":\"00ub0oNGTSWTBKOLGLHO\"," +
222+
"\"status\":\"ACTIVE\"," +
223+
"\"created\":\"2013-06-24T16:39:18.000Z\"," +
224+
"\"activated\":\"2013-06-24T16:39:19.000Z\"," +
225+
"\"statusChanged\":\"2013-06-24T16:39:19.000Z\"," +
226+
"\"lastLogin\":\"2013-10-02T14:06:25.000Z\"," +
227+
"\"lastUpdated\":\"2013-10-02T14:06:25.000Z\"," +
228+
"\"passwordChanged\":\"2013-09-11T23:30:26.000Z\"," +
229+
"\"profile\":{" +
230+
"\"firstName\":\"Jane\"," +
231+
"\"lastName\":\"Developer\"," +
232+
"\"email\":\"jane.developer@example.com\"," +
233+
"\"login\":\"jane.developer@example.com\"," +
234+
"\"mobilePhone\":null" +
235+
"}" +
236+
"}" +
237+
"]")
238+
));
239+
240+
// Call the SDK to list users
241+
List<User> users = userApi.listUsers(null, null, null, null, null, null, null, null, null, null);
242+
243+
// Verify the response
244+
assertNotNull(users);
245+
assertEquals(2, users.size());
246+
247+
User firstUser = users.get(0);
248+
assertEquals("00ub0oNGTSWTBKOLGLHN", firstUser.getId());
249+
assertEquals("isaac.brock@example.com", firstUser.getProfile().getEmail());
250+
251+
User secondUser = users.get(1);
252+
assertEquals("00ub0oNGTSWTBKOLGLHO", secondUser.getId());
253+
assertEquals("jane.developer@example.com", secondUser.getProfile().getEmail());
254+
}
255+
256+
@Test
257+
public void testWireMockHttps() {
258+
// This test simply verifies that the WireMock server is running on HTTPS
259+
// and the SSL context is properly configured
260+
assertNotNull(wireMockServer);
261+
assertNotNull(client);
262+
assertNotNull(userApi);
263+
}
264+
}
265+

0 commit comments

Comments
 (0)