Skip to content

Commit a6a00e7

Browse files
Removed extra md files, added description in Readme and made solution Thread safe
1 parent 9db88a6 commit a6a00e7

2 files changed

Lines changed: 253 additions & 20 deletions

File tree

README.md

Lines changed: 204 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,211 @@ ApiClient client = Clients.builder()
803804
```
804805
[//]: # (end: disableCaching)
805806

807+
## WireMock Integration Testing
808+
809+
### Overview
810+
811+
WireMock enables testing with the Okta Java SDK by providing a mock HTTP server that simulates Okta's API endpoints over HTTPS. This eliminates the need to hit actual Okta servers during development and testing, removing rate limit concerns and enabling rapid iteration.
812+
813+
### Problem
814+
815+
The Okta SDK requires HTTPS connections, and when using WireMock with self-signed certificates, the SDK's HTTP client must be configured to trust the mock server's certificate. This section demonstrates the complete setup.
816+
817+
### Solution Architecture
818+
819+
The solution consists of three components:
820+
821+
1. **Self-Signed Certificate Generation**: Automatically generates a JKS keystore with a self-signed certificate
822+
2. **WireMock HTTPS Configuration**: Configures WireMock server to use the certificate
823+
3. **Custom SSL Context**: Configures the Okta SDK's HTTP client to trust the certificate
824+
825+
### Implementation
826+
827+
#### Step 1: Automatic Certificate Generation
828+
829+
The test setup automatically generates a self-signed certificate on first run:
830+
831+
```java
832+
String keystorePath = Paths.get(KEYSTORE_PATH).toAbsolutePath().toString();
833+
java.io.File keystoreFile = new java.io.File(keystorePath);
834+
if (!keystoreFile.exists()) {
835+
ProcessBuilder pb = new ProcessBuilder(
836+
"keytool", "-genkey", "-alias", "wiremock", "-keyalg", "RSA",
837+
"-keystore", keystorePath,
838+
"-storepass", "password", "-keypass", "password",
839+
"-dname", "CN=localhost", "-validity", "365", "-noprompt"
840+
);
841+
int exitCode = pb.start().waitFor();
842+
if (exitCode != 0) {
843+
throw new RuntimeException("Failed to generate WireMock keystore");
844+
}
845+
}
846+
```
847+
848+
The certificate is generated once and reused for subsequent test runs. The `.gitignore` file excludes the keystore from version control to maintain security best practices.
849+
850+
#### Step 2: Configure WireMock Server
851+
852+
Start WireMock on HTTPS port 8443 using the generated certificate:
853+
854+
```java
855+
wireMockServer = new WireMockServer(
856+
WireMockConfiguration.wireMockConfig()
857+
.httpsPort(8443)
858+
.keystorePath(keystorePath)
859+
.keystorePassword("password")
860+
);
861+
wireMockServer.start();
862+
```
863+
864+
#### Step 3: Create Custom SSL Context
865+
866+
Load the keystore and configure an SSL context that trusts the self-signed certificate:
867+
868+
```java
869+
KeyStore trustStore = KeyStore.getInstance("JKS");
870+
try (FileInputStream fis = new FileInputStream(keystorePath)) {
871+
trustStore.load(fis, "password".toCharArray());
872+
}
873+
874+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
875+
TrustManagerFactory.getDefaultAlgorithm());
876+
tmf.init(trustStore);
877+
878+
SSLContext sslContext = SSLContext.getInstance("TLS");
879+
sslContext.init(null, tmf.getTrustManagers(), new java.security.SecureRandom());
880+
```
881+
882+
#### Step 4: Configure HTTP Client with Custom SSL Context
883+
884+
Create the HTTP client with dynamic port allocation for thread safety:
885+
886+
```java
887+
// Find an available port for thread-safe parallel execution
888+
ServerSocket socket = new ServerSocket(0);
889+
int wiremockPort = socket.getLocalPort();
890+
socket.close();
891+
892+
org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient =
893+
HttpClients.custom()
894+
.setConnectionManager(
895+
PoolingHttpClientConnectionManagerBuilder.create()
896+
.setSSLSocketFactory(
897+
new org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory(sslContext)
898+
)
899+
.build()
900+
)
901+
.build();
902+
```
903+
904+
#### Step 5: Create Okta API Client
905+
906+
Instantiate the API client with the custom HTTP client:
907+
908+
```java
909+
client = new ApiClient(httpClient, new com.okta.sdk.impl.cache.DisabledCacheManager());
910+
client.setBasePath("https://localhost:" + wiremockPort);
911+
912+
userApi = new UserApi(client);
913+
```
914+
915+
### Usage Example
916+
917+
Define API endpoint mocks using WireMock's stubFor pattern:
918+
919+
```java
920+
@Test
921+
public void testGetUser() throws ApiException {
922+
String userId = "00ub0oNGTSWTBKOLGLHN";
923+
924+
stubFor(get(urlEqualTo("/api/v1/users/" + userId))
925+
.willReturn(aResponse()
926+
.withStatus(200)
927+
.withHeader("Content-Type", "application/json")
928+
.withBody("{" +
929+
"\"id\":\"" + userId + "\"," +
930+
"\"status\":\"ACTIVE\"," +
931+
"\"profile\":{" +
932+
"\"email\":\"user@example.com\"" +
933+
"}" +
934+
"}")
935+
));
936+
937+
User user = userApi.getUser(userId, null, null);
938+
939+
assertNotNull(user);
940+
assertEquals(userId, user.getId());
941+
assertEquals("ACTIVE", user.getStatus().toString());
942+
}
943+
```
944+
945+
### Thread Safety
946+
947+
The implementation uses dynamic port allocation to ensure thread-safe parallel test execution:
948+
949+
```java
950+
ServerSocket socket = new ServerSocket(0); // OS assigns available port
951+
int wiremockPort = socket.getLocalPort();
952+
socket.close();
953+
```
954+
955+
This approach allows multiple test instances to run simultaneously, each with its own isolated WireMock server instance on a unique port.
956+
957+
### Reference Implementation
958+
959+
A complete working example is provided in `integration-tests/src/test/java/com/okta/sdk/tests/WireMockOktaClientTest.java` with test cases for:
960+
961+
- Single user retrieval (testGetUser)
962+
- User list operations (testListUsers)
963+
- HTTPS configuration verification (testWireMockHttps)
964+
965+
### Running the Tests
966+
967+
Execute the integration tests with:
968+
969+
```bash
970+
mvn test -Dtest=WireMockOktaClientTest -pl integration-tests
971+
```
972+
973+
Expected output:
974+
975+
```
976+
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
977+
```
978+
979+
### Prerequisites
980+
981+
- Java Development Kit (JDK) 8 or later
982+
- Apache Maven 3.6 or later
983+
- keytool (included with JDK)
984+
985+
### Troubleshooting
986+
987+
**Port Already in Use**
988+
989+
If port 8443 is in use, the dynamic port allocation in the current implementation automatically selects an available port. Ensure you pass the dynamically assigned port to the client configuration.
990+
991+
**Certificate Trust Issues**
992+
993+
Verify that the keystore file is generated and accessible:
994+
995+
```bash
996+
ls -la wiremock-keystore.jks
997+
```
998+
999+
If the file is missing or corrupted, delete it and run tests again to regenerate.
1000+
1001+
**keytool Not Found**
1002+
1003+
keytool is included with the JDK. Ensure JAVA_HOME is properly configured:
1004+
1005+
```bash
1006+
echo $JAVA_HOME
1007+
```
1008+
8061009
## Building the SDK
8071010

1011+
8081012
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).
8091013

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

integration-tests/src/test/java/com/okta/sdk/tests/WireMockOktaClientTest.java

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,45 +40,61 @@
4040
import static org.testng.Assert.assertNotNull;
4141
import static org.testng.Assert.assertEquals;
4242

43+
import java.net.ServerSocket;
44+
4345
/**
4446
* Integration test demonstrating WireMock + Okta SDK with HTTPS using self-signed certificates.
4547
* 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.
4651
*/
4752
public class WireMockOktaClientTest {
4853

4954
private WireMockServer wireMockServer;
5055
private ApiClient client;
5156
private UserApi userApi;
52-
private static final int WIREMOCK_HTTPS_PORT = 8443;
53-
private static final String WIREMOCK_HOST = "https://localhost:" + WIREMOCK_HTTPS_PORT;
57+
private int wireMockHttpsPort; // Dynamic port for thread-safety
58+
private String wireMockHost; // Computed from dynamic port
5459
private static final String KEYSTORE_PATH = "../../wiremock-keystore.jks"; // Path from integration-tests module
5560
private static final String KEYSTORE_PASSWORD = "password";
61+
private static final Object KEYSTORE_LOCK = new Object(); // Lock for thread-safe keystore generation
5662

5763
@BeforeMethod
5864
public void setup() throws Exception {
59-
// Generate WireMock keystore if it doesn't exist
65+
// Generate WireMock keystore if it doesn't exist (synchronized for thread-safety)
6066
String keystorePath = Paths.get(KEYSTORE_PATH).toAbsolutePath().toString();
61-
java.io.File keystoreFile = new java.io.File(keystorePath);
62-
if (!keystoreFile.exists()) {
63-
System.out.println("Generating WireMock keystore at: " + keystorePath);
64-
ProcessBuilder pb = new ProcessBuilder(
65-
"keytool", "-genkey", "-alias", "wiremock", "-keyalg", "RSA",
66-
"-keystore", keystorePath,
67-
"-storepass", KEYSTORE_PASSWORD, "-keypass", KEYSTORE_PASSWORD,
68-
"-dname", "CN=localhost", "-validity", "365", "-noprompt"
69-
);
70-
int exitCode = pb.start().waitFor();
71-
if (exitCode != 0) {
72-
throw new RuntimeException("Failed to generate WireMock keystore. " +
73-
"Ensure 'keytool' is in your PATH (comes with Java)");
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");
7485
}
75-
System.out.println("WireMock keystore generated successfully");
7686
}
7787

78-
// Start WireMock on HTTPS with self-signed certificate
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
7995
wireMockServer = new WireMockServer(
8096
WireMockConfiguration.wireMockConfig()
81-
.httpsPort(WIREMOCK_HTTPS_PORT)
97+
.httpsPort(wireMockHttpsPort)
8298
.keystorePath(KEYSTORE_PATH)
8399
.keystorePassword(KEYSTORE_PASSWORD)
84100
);
@@ -111,11 +127,24 @@ public void setup() throws Exception {
111127

112128
// Create ApiClient with the custom HttpClient and a disabled cache manager
113129
client = new ApiClient(httpClient, new com.okta.sdk.impl.cache.DisabledCacheManager());
114-
client.setBasePath(WIREMOCK_HOST);
130+
client.setBasePath(wireMockHost); // Use dynamic host with dynamic port
115131

116132
userApi = new UserApi(client);
117133
}
118134

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+
119148
@AfterMethod
120149
public void teardown() {
121150
if (wireMockServer != null) {

0 commit comments

Comments
 (0)