mirror of
https://github.com/google/nomulus
synced 2026-04-24 02:00:50 +00:00
Add mosapi client to intract with ICANN's monitoring system (#2892)
* Add mosapi client to intract with ICANN's monitoring system This change introduces a comprehensive client to interact with ICANN's Monitoring System API (MoSAPI). This provides direct, automated access to critical registry health and compliance data, moving Nomulus towards a more proactive monitoring posture. A core, stateless MosApiClient that manages communication and authentication with the MoSAPI service using TLS client certificates. * Resolve review feedback & upgrade to OkHttp3 client This commit addresses and resolves all outstanding review comments, primarily encompassing a shift to OkHttp3, security configuration cleanup, and general code improvements. * **Review:** Addressed and resolved all pending review comments. * **Feature:** Switched the underlying HTTP client implementation to [OkHttp3](https://square.github.io/okhttp/). * **Configuration:** Consolidated TLS Certificates-related configuration into the dedicated configuration area. * **Cleanup:** Removed unused components (`HttpUtils` and `HttpModule`) and performed general code cleanup. * **Quality:** Improved exception handling logic for better robustness. * Refactor and fix Mosapi exception handling Addresses code review feedback and resulting test failures. - Flattens package structure by moving MosApiException and its test. - Corrects exception handling to ensure MosApiAuthorizationException propagates correctly, before the general exception handler. - Adds a default case to the MosApiException factory for robustness. - Uses lowercase for placeholder TLDs in default-config.yaml. * Refactor and improve Mosapi client implementation Simplifying URL validation with Guava Preconditions and refining exception handling to use `Throwables`. * Refactor precondition checks using project specific utility
This commit is contained in:
@@ -34,6 +34,7 @@ import google.registry.keyring.KeyringModule;
|
||||
import google.registry.keyring.api.KeyModule;
|
||||
import google.registry.module.TestRequestComponent.TestRequestComponentModule;
|
||||
import google.registry.monitoring.whitebox.StackdriverModule;
|
||||
import google.registry.mosapi.module.MosApiModule;
|
||||
import google.registry.persistence.PersistenceModule;
|
||||
import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||
import google.registry.rde.JSchModule;
|
||||
@@ -61,6 +62,7 @@ import jakarta.inject.Singleton;
|
||||
GroupsModule.class,
|
||||
GroupssettingsModule.class,
|
||||
GsonModule.class,
|
||||
MosApiModule.class,
|
||||
JSchModule.class,
|
||||
KeyModule.class,
|
||||
KeyringModule.class,
|
||||
|
||||
203
core/src/test/java/google/registry/mosapi/MosApiClientTest.java
Normal file
203
core/src/test/java/google/registry/mosapi/MosApiClientTest.java
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 google.registry.mosapi;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.mosapi.MosApiException.MosApiAuthorizationException;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Protocol;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
public class MosApiClientTest {
|
||||
private static final String SERVICE_URL = "https://mosapi.example.com/v1";
|
||||
private static final String ENTITY_TYPE = "registries";
|
||||
|
||||
// Mocks
|
||||
private OkHttpClient mockHttpClient;
|
||||
private Call mockCall;
|
||||
|
||||
private MosApiClient mosApiClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockHttpClient = mock(OkHttpClient.class);
|
||||
mockCall = mock(Call.class);
|
||||
when(mockHttpClient.newCall(any(Request.class))).thenReturn(mockCall);
|
||||
mosApiClient = new MosApiClient(mockHttpClient, SERVICE_URL, ENTITY_TYPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructor_throwsOnInvalidUrl() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new MosApiClient(mockHttpClient, "ht tp://bad-url", ENTITY_TYPE));
|
||||
assertThat(thrown).hasMessageThat().contains("Invalid MoSAPI Service URL");
|
||||
}
|
||||
|
||||
// --- GET Request Tests ---
|
||||
|
||||
@Test
|
||||
void testSendGetRequest_success() throws Exception {
|
||||
// 1. Prepare Success Response
|
||||
Response successResponse = createResponse(200, "{\"status\":\"ok\"}");
|
||||
when(mockCall.execute()).thenReturn(successResponse);
|
||||
|
||||
Map<String, String> params = ImmutableMap.of("since", "2024-01-01");
|
||||
Map<String, String> headers = ImmutableMap.of("Authorization", "Bearer token123");
|
||||
|
||||
// 2. Execute
|
||||
try (Response response =
|
||||
mosApiClient.sendGetRequest("tld-1", "monitoring/state", params, headers)) {
|
||||
|
||||
// Verify Response
|
||||
assertThat(response.isSuccessful()).isTrue();
|
||||
assertThat(response.body().string()).isEqualTo("{\"status\":\"ok\"}");
|
||||
|
||||
// 3. Verify Request Construction
|
||||
ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
|
||||
verify(mockHttpClient).newCall(requestCaptor.capture());
|
||||
Request capturedRequest = requestCaptor.getValue();
|
||||
|
||||
// Check URL:
|
||||
assertThat(capturedRequest.method()).isEqualTo("GET");
|
||||
assertThat(capturedRequest.url().encodedPath())
|
||||
.isEqualTo("/v1/registries/tld-1/monitoring/state");
|
||||
assertThat(capturedRequest.url().queryParameter("since")).isEqualTo("2024-01-01");
|
||||
|
||||
// Check Headers
|
||||
assertThat(capturedRequest.header("Authorization")).isEqualTo("Bearer token123");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendGetRequest_throwsOn401() throws IOException {
|
||||
// Prepare 401 Response
|
||||
Response unauthorizedResponse = createResponse(401, "Unauthorized");
|
||||
when(mockCall.execute()).thenReturn(unauthorizedResponse);
|
||||
|
||||
MosApiAuthorizationException thrown =
|
||||
assertThrows(
|
||||
MosApiAuthorizationException.class,
|
||||
() ->
|
||||
mosApiClient.sendGetRequest("tld-1", "path", ImmutableMap.of(), ImmutableMap.of()));
|
||||
|
||||
assertThat(thrown).hasMessageThat().contains("Authorization failed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendGetRequest_wrapsIoException() throws IOException {
|
||||
// Simulate Network Failure
|
||||
when(mockCall.execute()).thenThrow(new IOException("Network error"));
|
||||
|
||||
// Execute & Assert
|
||||
MosApiException thrown =
|
||||
assertThrows(
|
||||
MosApiException.class,
|
||||
() ->
|
||||
mosApiClient.sendGetRequest("tld-1", "path", ImmutableMap.of(), ImmutableMap.of()));
|
||||
|
||||
assertThat(thrown).hasMessageThat().contains("Error during GET request to");
|
||||
assertThat(thrown).hasCauseThat().isInstanceOf(IOException.class);
|
||||
}
|
||||
|
||||
// --- POST Request Tests ---
|
||||
|
||||
@Test
|
||||
void testSendPostRequest_success() throws Exception {
|
||||
// 1. Prepare Response
|
||||
Response successResponse = createResponse(200, "{\"updated\":true}");
|
||||
when(mockCall.execute()).thenReturn(successResponse);
|
||||
|
||||
String requestBody = "{\"data\":\"update\"}";
|
||||
Map<String, String> headers = ImmutableMap.of("Content-Type", "application/json");
|
||||
|
||||
try (Response response =
|
||||
mosApiClient.sendPostRequest("tld-1", "update", null, headers, requestBody)) {
|
||||
|
||||
assertThat(response.isSuccessful()).isTrue();
|
||||
|
||||
ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
|
||||
verify(mockHttpClient).newCall(requestCaptor.capture());
|
||||
Request capturedRequest = requestCaptor.getValue();
|
||||
|
||||
assertThat(capturedRequest.method()).isEqualTo("POST");
|
||||
assertThat(capturedRequest.url().encodedPath()).isEqualTo("/v1/registries/tld-1/update");
|
||||
|
||||
// Verify Body content
|
||||
Buffer buffer = new Buffer();
|
||||
capturedRequest.body().writeTo(buffer);
|
||||
assertThat(buffer.readUtf8()).isEqualTo(requestBody);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendPostRequest_throwsOn401() throws IOException {
|
||||
// Prepare 401 Response
|
||||
Response unauthorizedResponse = createResponse(401, "Unauthorized");
|
||||
when(mockCall.execute()).thenReturn(unauthorizedResponse);
|
||||
|
||||
MosApiAuthorizationException thrown =
|
||||
assertThrows(
|
||||
MosApiAuthorizationException.class,
|
||||
() ->
|
||||
mosApiClient.sendPostRequest(
|
||||
"tld-1", "path", ImmutableMap.of(), ImmutableMap.of(), "{}"));
|
||||
|
||||
assertThat(thrown).hasMessageThat().contains("Authorization failed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendPostRequest_wrapsIoException() throws IOException {
|
||||
// Simulate Network Failure
|
||||
when(mockCall.execute()).thenThrow(new IOException("Network error"));
|
||||
MosApiException thrown =
|
||||
assertThrows(
|
||||
MosApiException.class,
|
||||
() ->
|
||||
mosApiClient.sendPostRequest(
|
||||
"tld-1", "path", ImmutableMap.of(), ImmutableMap.of(), "{}"));
|
||||
assertThat(thrown).hasMessageThat().contains("Error during POST request to");
|
||||
assertThat(thrown).hasCauseThat().isInstanceOf(IOException.class);
|
||||
}
|
||||
|
||||
/** Helper to build a real OkHttp Response object manually. */
|
||||
private Response createResponse(int code, String bodyContent) {
|
||||
return new Response.Builder()
|
||||
.request(new Request.Builder().url("http://localhost/").build())
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(code)
|
||||
.message("Msg")
|
||||
.body(ResponseBody.create(bodyContent, MediaType.parse("application/json")))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 google.registry.mosapi;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import google.registry.mosapi.MosApiException.DateDurationInvalidException;
|
||||
import google.registry.mosapi.MosApiException.DateOrderInvalidException;
|
||||
import google.registry.mosapi.MosApiException.EndDateSyntaxInvalidException;
|
||||
import google.registry.mosapi.MosApiException.MosApiAuthorizationException;
|
||||
import google.registry.mosapi.MosApiException.StartDateSyntaxInvalidException;
|
||||
import google.registry.mosapi.model.MosApiErrorResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link MosApiException}. */
|
||||
public class MosApiExceptionTest {
|
||||
|
||||
@Test
|
||||
void testConstructor_withErrorResponse() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("1234", "Test Message", "Test Description");
|
||||
MosApiException exception = new MosApiException(errorResponse);
|
||||
assertThat(exception)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("MoSAPI returned an error (code: 1234): Test Message");
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructor_withMessageAndCause() {
|
||||
RuntimeException cause = new RuntimeException("Root Cause");
|
||||
MosApiException exception = new MosApiException("Wrapper Message", cause);
|
||||
assertThat(exception).hasMessageThat().isEqualTo("Wrapper Message");
|
||||
assertThat(exception).hasCauseThat().isEqualTo(cause);
|
||||
assertThat(exception.getErrorResponse()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAuthorizationException() {
|
||||
MosApiAuthorizationException exception = new MosApiAuthorizationException("Unauthorized");
|
||||
assertThat(exception).isInstanceOf(MosApiException.class);
|
||||
assertThat(exception).hasMessageThat().isEqualTo("Unauthorized");
|
||||
assertThat(exception.getErrorResponse()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forDateDurationInvalid() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("2011", "Duration invalid", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception).isInstanceOf(DateDurationInvalidException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forDateOrderInvalid() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("2012", "End date before start date", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception).isInstanceOf(DateOrderInvalidException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forStartDateSyntaxInvalid() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("2013", "Invalid start date format", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception).isInstanceOf(StartDateSyntaxInvalidException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forEndDateSyntaxInvalid() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("2014", "Invalid end date format", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception).isInstanceOf(EndDateSyntaxInvalidException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forUnknownCode() {
|
||||
MosApiErrorResponse errorResponse = new MosApiErrorResponse("9999", "Unknown", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception.getClass()).isEqualTo(MosApiException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 google.registry.mosapi.model;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link MosApiErrorResponse}. */
|
||||
public class MosApiErrorResponseTest {
|
||||
|
||||
@Test
|
||||
void testJsonDeserialization() {
|
||||
String json =
|
||||
"""
|
||||
{
|
||||
"resultCode": "2012",
|
||||
"message": "The endDate is before the startDate.",
|
||||
"description": "Validation failed"
|
||||
}
|
||||
""";
|
||||
|
||||
MosApiErrorResponse response = new Gson().fromJson(json, MosApiErrorResponse.class);
|
||||
|
||||
assertThat(response.resultCode()).isEqualTo("2012");
|
||||
assertThat(response.message()).isEqualTo("The endDate is before the startDate.");
|
||||
assertThat(response.description()).isEqualTo("Validation failed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 google.registry.mosapi.module;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.privileges.secretmanager.SecretManagerClient;
|
||||
import java.io.StringWriter;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Optional;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.bouncycastle.asn1.ASN1GeneralizedTime;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.asn1.x509.Time;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class MosApiModuleTest {
|
||||
|
||||
private static final String TEST_CERT_SECRET_NAME = "testCert";
|
||||
private static final String TEST_KEY_SECRET_NAME = "testKey";
|
||||
|
||||
private SecretManagerClient secretManagerClient;
|
||||
private String validCertPem;
|
||||
private String validKeyPem;
|
||||
private PrivateKey generatedPrivateKey;
|
||||
private X509Certificate generatedCertificate;
|
||||
|
||||
@BeforeAll
|
||||
static void setupStatics() {
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
secretManagerClient = mock(SecretManagerClient.class);
|
||||
generateTestCredentials();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideMosapiTlsCert_fetchesFromConfiguredSecretName() {
|
||||
when(secretManagerClient.getSecretData(any(), any())).thenReturn(validCertPem);
|
||||
String result = MosApiModule.provideMosapiTlsCert(secretManagerClient, TEST_CERT_SECRET_NAME);
|
||||
assertThat(result).isEqualTo(validCertPem);
|
||||
verify(secretManagerClient).getSecretData(eq(TEST_CERT_SECRET_NAME), eq(Optional.of("latest")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideMosapiTlsKey_fetchesFromConfiguredSecretName() {
|
||||
when(secretManagerClient.getSecretData(any(), any())).thenReturn(validKeyPem);
|
||||
String result = MosApiModule.provideMosapiTlsKey(secretManagerClient, TEST_KEY_SECRET_NAME);
|
||||
assertThat(result).isEqualTo(validKeyPem);
|
||||
verify(secretManagerClient).getSecretData(eq(TEST_KEY_SECRET_NAME), eq(Optional.of("latest")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideCertificate_parsesValidPem() {
|
||||
Certificate cert = MosApiModule.provideCertificate(validCertPem);
|
||||
assertThat(cert).isInstanceOf(X509Certificate.class);
|
||||
// Verify the public key matches to ensure we parsed the correct cert
|
||||
assertThat(cert.getPublicKey()).isEqualTo(generatedCertificate.getPublicKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideCertificate_throwsOnInvalidPem() {
|
||||
RuntimeException thrown =
|
||||
assertThrows(
|
||||
RuntimeException.class, () -> MosApiModule.provideCertificate("NOT A REAL CERT"));
|
||||
assertThat(thrown).hasMessageThat().contains("Could not create X.509 certificate");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvidePrivateKey_parsesValidPem() {
|
||||
PrivateKey key = MosApiModule.providePrivateKey(validKeyPem);
|
||||
assertThat(key).isNotNull();
|
||||
assertThat(key.getAlgorithm()).isEqualTo("RSA");
|
||||
assertThat(key.getEncoded()).isEqualTo(generatedPrivateKey.getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvidePrivateKey_throwsOnInvalidPem() {
|
||||
RuntimeException thrown =
|
||||
assertThrows(
|
||||
RuntimeException.class, () -> MosApiModule.providePrivateKey("NOT A REAL KEY"));
|
||||
assertThat(thrown).hasMessageThat().contains("Could not parse TLS private key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideKeyStore_createsWithCorrectAlias() throws Exception {
|
||||
KeyStore keyStore = MosApiModule.provideKeyStore(generatedPrivateKey, generatedCertificate);
|
||||
assertThat(keyStore).isNotNull();
|
||||
assertThat(keyStore.getType()).isEqualTo("PKCS12");
|
||||
assertThat(keyStore.containsAlias("client")).isTrue();
|
||||
assertThat(keyStore.getCertificate("client")).isEqualTo(generatedCertificate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideMosapiHttpClient_usesConfiguredSslContext() {
|
||||
SSLContext mockSslContext = mock(SSLContext.class);
|
||||
SSLSocketFactory mockSocketFactory = mock(SSLSocketFactory.class);
|
||||
X509TrustManager mockTrustManager = mock(X509TrustManager.class);
|
||||
when(mockTrustManager.getAcceptedIssuers()).thenReturn(new X509Certificate[0]);
|
||||
when(mockSslContext.getSocketFactory()).thenReturn(mockSocketFactory);
|
||||
OkHttpClient client = MosApiModule.provideMosapiHttpClient(mockSslContext, mockTrustManager);
|
||||
assertThat(client).isNotNull();
|
||||
assertThat(client.sslSocketFactory()).isEqualTo(mockSocketFactory);
|
||||
}
|
||||
|
||||
private void generateTestCredentials() throws Exception {
|
||||
// 1. Generate KeyPair
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048);
|
||||
KeyPair keyPair = keyGen.generateKeyPair();
|
||||
this.generatedPrivateKey = keyPair.getPrivate();
|
||||
DateTimeFormatter formatter =
|
||||
DateTimeFormatter.ofPattern("yyyyMMddHHmmss'Z'").withZone(ZoneId.of("UTC"));
|
||||
Instant now = Instant.now();
|
||||
Instant end = now.plus(Duration.ofDays(365));
|
||||
// Convert string to Bouncy Castle Time objects
|
||||
Time notBefore = new Time(new ASN1GeneralizedTime(formatter.format(now)));
|
||||
Time notAfter = new Time(new ASN1GeneralizedTime(formatter.format(end)));
|
||||
X509v3CertificateBuilder certBuilder =
|
||||
new X509v3CertificateBuilder(
|
||||
new X500Name("CN=Test"),
|
||||
BigInteger.valueOf(now.toEpochMilli()),
|
||||
notBefore,
|
||||
notAfter,
|
||||
new X500Name("CN=Test"),
|
||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||
ContentSigner contentSigner =
|
||||
new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate());
|
||||
this.generatedCertificate =
|
||||
new JcaX509CertificateConverter()
|
||||
.setProvider("BC")
|
||||
.getCertificate(certBuilder.build(contentSigner));
|
||||
// 4. Convert to PEM Strings
|
||||
this.validCertPem = toPem(generatedCertificate);
|
||||
this.validKeyPem = toPem(generatedPrivateKey);
|
||||
}
|
||||
|
||||
private String toPem(Object object) throws Exception {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
|
||||
pemWriter.writeObject(object);
|
||||
}
|
||||
return stringWriter.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user