From 7e5a49c95d4645b06737f591d325294b4da680bc Mon Sep 17 00:00:00 2001 From: johncooper Date: Thu, 18 Jul 2024 15:23:40 +1200 Subject: [PATCH] [LT-1142] Use API URL if provided. --- .../java/com/sailthru/sqs/ApiFactory.java | 14 ++ .../com/sailthru/sqs/MParticleClient.java | 25 ++- .../sqs/message/MParticleOutgoingMessage.java | 9 + .../com/sailthru/sqs/MParticleClientTest.java | 162 ++++++++++++++++++ .../sailthru/sqs/MessageProcessorTest.java | 2 +- src/test/resources/messages/invalid1.json | 1 + src/test/resources/messages/invalid2.json | 1 + src/test/resources/messages/valid.json | 7 +- .../resources/messages/validWithoutURL.json | 27 +++ .../resources/messages/validWithoutURL2.json | 26 +++ 10 files changed, 262 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/sailthru/sqs/ApiFactory.java create mode 100644 src/test/java/com/sailthru/sqs/MParticleClientTest.java create mode 100644 src/test/resources/messages/validWithoutURL.json create mode 100644 src/test/resources/messages/validWithoutURL2.json diff --git a/src/main/java/com/sailthru/sqs/ApiFactory.java b/src/main/java/com/sailthru/sqs/ApiFactory.java new file mode 100644 index 0000000..6691a11 --- /dev/null +++ b/src/main/java/com/sailthru/sqs/ApiFactory.java @@ -0,0 +1,14 @@ +package com.sailthru.sqs; + +import com.mparticle.ApiClient; +import com.mparticle.client.EventsApi; + +public class ApiFactory { + public EventsApi create(final String apiKey, final String apiSecret, final String apiURL) { + final ApiClient apiClient = new ApiClient(apiKey, apiSecret); + + apiClient.getAdapterBuilder().baseUrl(apiURL); + + return apiClient.createService(EventsApi.class); + } +} diff --git a/src/main/java/com/sailthru/sqs/MParticleClient.java b/src/main/java/com/sailthru/sqs/MParticleClient.java index 8aeabf2..86545e6 100644 --- a/src/main/java/com/sailthru/sqs/MParticleClient.java +++ b/src/main/java/com/sailthru/sqs/MParticleClient.java @@ -1,6 +1,5 @@ package com.sailthru.sqs; -import com.mparticle.ApiClient; import com.mparticle.client.EventsApi; import com.mparticle.model.Batch; import com.mparticle.model.CustomEvent; @@ -16,20 +15,24 @@ import java.io.IOException; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; +import java.util.Optional; import static java.lang.String.format; import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; import static java.util.Optional.ofNullable; +import static java.util.function.Predicate.not; public class MParticleClient { private static final Logger LOGGER = LoggerFactory.getLogger(MessageProcessor.class); - private static final String BASE_URL = "https://inbound.mparticle.com/s2s/v2/"; + static final String DEFAULT_BASE_URL = "https://inbound.mparticle.com/s2s/v2/"; + + private ApiFactory apiFactory; public void submit(final MParticleOutgoingMessage message) throws RetryLaterException { final Batch batch = prepareBatch(message); - final EventsApi eventsApi = getEventsApi(message.getAuthenticationKey(), message.getAuthenticationSecret()); + final EventsApi eventsApi = getEventsApi(message); LOGGER.debug("Attempting to send batch: {} for message: {}", batch, message); @@ -50,12 +53,14 @@ public void submit(final MParticleOutgoingMessage message) throws RetryLaterExce } } - private EventsApi getEventsApi(final String apiKey, final String apiSecret) { - final ApiClient apiClient = new ApiClient(apiKey, apiSecret); - - apiClient.getAdapterBuilder().baseUrl(BASE_URL); + private EventsApi getEventsApi(final MParticleOutgoingMessage message) { + final String apiKey = message.getAuthenticationKey(); + final String apiSecret = message.getAuthenticationSecret(); + final String apiURL = Optional.ofNullable(message.getApiURL()) + .filter(not(String::isEmpty)) + .orElse(DEFAULT_BASE_URL); - return apiClient.createService(EventsApi.class); + return apiFactory.create(apiKey, apiSecret, apiURL); } private Batch prepareBatch(final MParticleOutgoingMessage message) { @@ -92,4 +97,8 @@ private Long parseTimestamp(String timestamp) { } return null; } + + public void setApiFactory(final ApiFactory apiFactory) { + this.apiFactory = apiFactory; + } } diff --git a/src/main/java/com/sailthru/sqs/message/MParticleOutgoingMessage.java b/src/main/java/com/sailthru/sqs/message/MParticleOutgoingMessage.java index 2be587d..4ae308d 100644 --- a/src/main/java/com/sailthru/sqs/message/MParticleOutgoingMessage.java +++ b/src/main/java/com/sailthru/sqs/message/MParticleOutgoingMessage.java @@ -12,6 +12,7 @@ public class MParticleOutgoingMessage { private String profileEmail; private List events; private String timestamp; + private String apiURL; public String getAuthenticationKey() { return authenticationKey; @@ -53,6 +54,14 @@ public String getTimestamp() { return timestamp; } + public String getApiURL() { + return apiURL; + } + + public void setApiURL(String apiURL) { + this.apiURL = apiURL; + } + @JsonIgnoreProperties(ignoreUnknown = true) public static class Event { private MParticleEventName eventName; diff --git a/src/test/java/com/sailthru/sqs/MParticleClientTest.java b/src/test/java/com/sailthru/sqs/MParticleClientTest.java new file mode 100644 index 0000000..d72bfba --- /dev/null +++ b/src/test/java/com/sailthru/sqs/MParticleClientTest.java @@ -0,0 +1,162 @@ +package com.sailthru.sqs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mparticle.client.EventsApi; +import com.mparticle.model.Batch; +import com.mparticle.model.CustomEvent; +import com.mparticle.model.CustomEventData; +import com.mparticle.model.UserIdentities; +import com.sailthru.sqs.exception.RetryLaterException; +import com.sailthru.sqs.message.MParticleEventName; +import com.sailthru.sqs.message.MParticleOutgoingMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import retrofit2.Call; +import retrofit2.Response; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; + +import static com.mparticle.model.CustomEvent.EventTypeEnum.CUSTOM_EVENT; +import static com.sailthru.sqs.MParticleClient.DEFAULT_BASE_URL; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MParticleClientTest { + @InjectMocks + private MParticleClient testInstance; + + @Mock + private ApiFactory mockApiFactory; + + @Mock + private EventsApi mockEventsApi; + + @Captor + private ArgumentCaptor batchCaptor; + + @Mock + private Call mockCall; + + @Mock + private Response mockResponse; + + @BeforeEach + void setUp() throws Exception { + when(mockApiFactory.create(anyString(), anyString(), anyString())).thenReturn(mockEventsApi); + when(mockEventsApi.uploadEvents(any(Batch.class))).thenReturn(mockCall); + when(mockCall.execute()).thenReturn(mockResponse); + } + + @Test + void givenUnsuccessfulResponseThenRetryLaterExceptionIsThrown() throws Exception { + final MParticleOutgoingMessage validMessageWithURL = givenValidMessage("/messages/valid.json"); + + when(mockResponse.isSuccessful()).thenReturn(false); + + assertThrows(RetryLaterException.class, () -> testInstance.submit(validMessageWithURL)); + } + + @Test + void givenIOExceptionThenRetryLaterExceptionIsThrown() throws Exception { + final MParticleOutgoingMessage validMessageWithURL = givenValidMessage("/messages/valid.json"); + + when(mockCall.execute()).thenThrow(IOException.class); + + assertThrows(RetryLaterException.class, () -> testInstance.submit(validMessageWithURL)); + } + + @Test + void givenValidMessageTheCorrectBatchIsSent() throws Exception { + final MParticleOutgoingMessage validMessageWithURL = givenValidMessage("/messages/valid.json"); + + testInstance.submit(validMessageWithURL); + + verify(mockEventsApi).uploadEvents(batchCaptor.capture()); + + final Batch result = batchCaptor.getValue(); + final UserIdentities userIdentities = result.getUserIdentities(); + assertThat(userIdentities.getEmail(), is(equalTo("REDACTED@gmail.com"))); + assertThat(result.getEvents().size(), is(2)); + + final CustomEvent event1 = (CustomEvent) result.getEvents().get(0); + assertThat(event1.getEventType(), is(CUSTOM_EVENT)); + + final CustomEventData event1Data = event1.getData(); + assertThat(event1Data.getEventName(), is(equalTo(MParticleEventName.EMAIL_SUBSCRIBE.name()))); + assertThat(event1Data.getCustomEventType(), is(equalTo(CustomEventData.CustomEventType.OTHER))); + + final Map customAttributes1 = event1Data.getCustomAttributes(); + assertThat(customAttributes1, hasEntry("client_id", "3386")); + assertThat(customAttributes1, hasEntry("profile_id", "6634e1bd31a2a0e8af0b0dff")); + assertThat(customAttributes1, hasEntry("list_id", "5609b2641aa312d6318b456b")); + + final CustomEvent event2 = (CustomEvent) result.getEvents().get(1); + assertThat(event2.getEventType(), is(CUSTOM_EVENT)); + + final CustomEventData event2Data = event2.getData(); + assertThat(event2Data.getEventName(), is(equalTo(MParticleEventName.EMAIL_UNSUBSCRIBE.name()))); + assertThat(event2Data.getCustomEventType(), is(equalTo(CustomEventData.CustomEventType.OTHER))); + + final Map customAttributes2 = event2Data.getCustomAttributes(); + assertThat(customAttributes2, hasEntry("client_id", "3386")); + assertThat(customAttributes2, hasEntry("profile_id", "7634e1bd31a2a0e8af0b0dfg")); + assertThat(customAttributes2, hasEntry("list_id", "6609b2641aa312d6318b456d")); + } + + @Test + void givenApiURLIsProvidedInMessageThenItIsUsed() throws Exception { + final MParticleOutgoingMessage validMessageWithURL = givenValidMessage("/messages/valid.json"); + + testInstance.submit(validMessageWithURL); + + verify(mockApiFactory).create("test_key", "test_secret", "https://test_url.com"); + } + + @Test + void givenApiURLIsNullInMessageThenDefaultIsUsed() throws Exception { + final MParticleOutgoingMessage validMessage = givenValidMessage("/messages/validWithoutURL.json"); + + testInstance.submit(validMessage); + + verify(mockApiFactory).create("test_key", "test_secret", DEFAULT_BASE_URL); + } + + @Test + void givenApiURLIsMissingThenDefaultIsUsed() throws Exception { + final MParticleOutgoingMessage validMessage = givenValidMessage("/messages/validWithoutURL2.json"); + + testInstance.submit(validMessage); + + verify(mockApiFactory).create("test_key", "test_secret", DEFAULT_BASE_URL); + } + + private MParticleOutgoingMessage givenValidMessage(final String filePath) throws Exception { + lenient().when(mockResponse.isSuccessful()).thenReturn(true); + + final String json = loadResourceFileContent(filePath); + return new ObjectMapper().readValue(json, MParticleOutgoingMessage.class); + } + + private String loadResourceFileContent(final String path) throws IOException, URISyntaxException { + return Files.readString(Paths.get(getClass().getResource(path).toURI())); + } +} diff --git a/src/test/java/com/sailthru/sqs/MessageProcessorTest.java b/src/test/java/com/sailthru/sqs/MessageProcessorTest.java index dce6e14..809b508 100644 --- a/src/test/java/com/sailthru/sqs/MessageProcessorTest.java +++ b/src/test/java/com/sailthru/sqs/MessageProcessorTest.java @@ -80,7 +80,7 @@ void givenValidPayloadProvidedThenMessageSubmitted() throws Exception { assertThat(event1.getEventType(), is(equalTo(MParticleEventType.OTHER))); final MParticleOutgoingMessage.Event event2 = message.getEvents().get(1); - assertThat(event2.getEventName(), is(MParticleEventName.EMAIL_SUBSCRIBE)); + assertThat(event2.getEventName(), is(MParticleEventName.EMAIL_UNSUBSCRIBE)); assertThat(event2.getEventType(), is(equalTo(MParticleEventType.OTHER))); } diff --git a/src/test/resources/messages/invalid1.json b/src/test/resources/messages/invalid1.json index dfd57a1..36f9236 100644 --- a/src/test/resources/messages/invalid1.json +++ b/src/test/resources/messages/invalid1.json @@ -1,6 +1,7 @@ { "authenticationKey": "", "authenticationSecret": "test_secret", + "apiURL": null, "profileEmail": "REDACTED@gmail.com", "events": [ { diff --git a/src/test/resources/messages/invalid2.json b/src/test/resources/messages/invalid2.json index e34b2d1..97abf2f 100644 --- a/src/test/resources/messages/invalid2.json +++ b/src/test/resources/messages/invalid2.json @@ -1,6 +1,7 @@ { "authenticationKey": "test_key", "authenticationSecret": "", + "apiURL": "https://test_url.com", "profileEmail": "REDACTED@gmail.com", "events": [ { diff --git a/src/test/resources/messages/valid.json b/src/test/resources/messages/valid.json index 4755f40..0257860 100644 --- a/src/test/resources/messages/valid.json +++ b/src/test/resources/messages/valid.json @@ -1,6 +1,7 @@ { "authenticationKey": "test_key", "authenticationSecret": "test_secret", + "apiURL": "https://test_url.com", "profileEmail": "REDACTED@gmail.com", "events": [ { @@ -13,12 +14,12 @@ } }, { - "eventName": "EMAIL_SUBSCRIBE", + "eventName": "EMAIL_UNSUBSCRIBE", "eventType": "OTHER", "additionalData": { "client_id": "3386", - "profile_id": "6634e1bd31a2a0e8af0b0dff", - "list_id": "5609b2641aa312d6318b456c" + "profile_id": "7634e1bd31a2a0e8af0b0dfg", + "list_id": "6609b2641aa312d6318b456d" } } ], diff --git a/src/test/resources/messages/validWithoutURL.json b/src/test/resources/messages/validWithoutURL.json new file mode 100644 index 0000000..d0a9bc4 --- /dev/null +++ b/src/test/resources/messages/validWithoutURL.json @@ -0,0 +1,27 @@ +{ + "authenticationKey": "test_key", + "authenticationSecret": "test_secret", + "apiURL": null, + "profileEmail": "REDACTED@gmail.com", + "events": [ + { + "eventName": "EMAIL_SUBSCRIBE", + "eventType": "OTHER", + "additionalData": { + "client_id": "3386", + "profile_id": "6634e1bd31a2a0e8af0b0dff", + "list_id": "5609b2641aa312d6318b456b" + } + }, + { + "eventName": "EMAIL_SUBSCRIBE", + "eventType": "OTHER", + "additionalData": { + "client_id": "3386", + "profile_id": "6634e1bd31a2a0e8af0b0dff", + "list_id": "5609b2641aa312d6318b456c" + } + } + ], + "timestamp": "2024-05-03T13:11:17Z[UTC]" +} diff --git a/src/test/resources/messages/validWithoutURL2.json b/src/test/resources/messages/validWithoutURL2.json new file mode 100644 index 0000000..4755f40 --- /dev/null +++ b/src/test/resources/messages/validWithoutURL2.json @@ -0,0 +1,26 @@ +{ + "authenticationKey": "test_key", + "authenticationSecret": "test_secret", + "profileEmail": "REDACTED@gmail.com", + "events": [ + { + "eventName": "EMAIL_SUBSCRIBE", + "eventType": "OTHER", + "additionalData": { + "client_id": "3386", + "profile_id": "6634e1bd31a2a0e8af0b0dff", + "list_id": "5609b2641aa312d6318b456b" + } + }, + { + "eventName": "EMAIL_SUBSCRIBE", + "eventType": "OTHER", + "additionalData": { + "client_id": "3386", + "profile_id": "6634e1bd31a2a0e8af0b0dff", + "list_id": "5609b2641aa312d6318b456c" + } + } + ], + "timestamp": "2024-05-03T13:11:17Z[UTC]" +}