|
13 | 13 | * [Thread safety considerations](#thread-safety-considerations) |
14 | 14 | * [Spring Support](#spring-support) |
15 | 15 | * [Configuration reference](#configuration-reference) |
| 16 | +* [WireMock Integration Testing](#wiremock-integration-testing) |
16 | 17 | * [Building the SDK](#building-the-sdk) |
17 | 18 | * [Contributing](#contributing) |
18 | 19 |
|
@@ -803,8 +804,211 @@ ApiClient client = Clients.builder() |
803 | 804 | ``` |
804 | 805 | [//]: # (end: disableCaching) |
805 | 806 |
|
| 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 | + |
806 | 1009 | ## Building the SDK |
807 | 1010 |
|
| 1011 | + |
808 | 1012 | 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). |
809 | 1013 |
|
810 | 1014 | > **Note**: The SDK uses a large OpenAPI specification file (~84,000 lines). If you encounter memory issues during build: |
|
0 commit comments