Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Voice E2E #161

Merged
merged 11 commits into from
Oct 2, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.sinch.sdk.core.exceptions.ApiAuthException;
import com.sinch.sdk.core.exceptions.ApiException;
import com.sinch.sdk.core.http.AuthManager;
import com.sinch.sdk.core.utils.MapUtils;
import com.sinch.sdk.core.utils.Pair;
import com.sinch.sdk.core.utils.StringUtil;
import java.net.URI;
Expand All @@ -15,7 +16,6 @@
import java.util.Base64;
import java.util.Collection;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
Expand Down Expand Up @@ -107,9 +107,7 @@ public boolean validateAuthenticatedRequest(
String method, String path, Map<String, String> headers, String jsonPayload) {

// convert header keys to use case-insensitive map keys
Map<String, String> caseInsensitiveHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
caseInsensitiveHeaders.putAll(headers);

Map<String, String> caseInsensitiveHeaders = MapUtils.getCaseInsensitiveMap(headers);
String authorizationHeader = caseInsensitiveHeaders.get("Authorization");

// missing authorization header
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.sinch.sdk.core.exceptions.ApiMappingException;
import com.sinch.sdk.core.http.AuthManager;
import com.sinch.sdk.core.utils.MapUtils;
import com.sinch.sdk.core.utils.databind.Mapper;
import com.sinch.sdk.domains.voice.adapters.converters.CallsDtoConverter;
import com.sinch.sdk.domains.voice.adapters.converters.WebhooksEventDtoConverter;
Expand All @@ -11,7 +12,6 @@
import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl;
import com.sinch.sdk.domains.voice.models.webhooks.WebhooksEvent;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;

public class WebHooksService implements com.sinch.sdk.domains.voice.WebHooksService {
Expand All @@ -27,8 +27,7 @@ public boolean validateAuthenticatedRequest(
String method, String path, Map<String, String> headers, String jsonPayload) {

// convert header keys to use case-insensitive map keys
Map<String, String> caseInsensitiveHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
caseInsensitiveHeaders.putAll(headers);
Map<String, String> caseInsensitiveHeaders = MapUtils.getCaseInsensitiveMap(headers);

String authorizationHeader = caseInsensitiveHeaders.get("Authorization");

Expand Down
13 changes: 13 additions & 0 deletions client/src/test/java/com/sinch/sdk/e2e/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.sinch.sdk.models.Configuration;
import com.sinch.sdk.models.ConversationContext;
import com.sinch.sdk.models.ConversationRegion;
import com.sinch.sdk.models.VoiceContext;

public class Config {

Expand All @@ -15,6 +16,11 @@ public class Config {
public static final String CONVERSATION_TEMPLATE_HOST_NAME = "http://localhost:3015";
public static final ConversationRegion CONVERSATION_REGION = ConversationRegion.US;

public static final String APPLICATION_KEY = "appKey";
public static final String APPLICATION_SECRET = "YXBwU2VjcmV0";
public static final String VOICE_HOST_NAME = "http://localhost:3019";
public static final String VOICE_MANAGEMENT_HOST_NAME = "http://localhost:3020";

private final SinchClient client;

private Config() {
Expand All @@ -31,6 +37,13 @@ private Config() {
.setRegion(Config.CONVERSATION_REGION)
.setTemplateManagementUrl(CONVERSATION_TEMPLATE_HOST_NAME)
.build())
.setApplicationKey(APPLICATION_KEY)
.setApplicationSecret(APPLICATION_SECRET)
.setVoiceContext(
VoiceContext.builder()
.setVoiceApplicationMngmtUrl(VOICE_MANAGEMENT_HOST_NAME)
.setVoiceUrl(VOICE_HOST_NAME)
.build())
.build();

client = new SinchClient(configuration);
Expand Down
52 changes: 52 additions & 0 deletions client/src/test/java/com/sinch/sdk/e2e/domains/WebhooksHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.sinch.sdk.e2e.domains;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public class WebhooksHelper {

public static <T> Response<T> callURL(URL url, Function<String, T> parseEvent)
throws IOException {

HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

byte[] buffer = new byte[1024];
int bytesRead;
try (InputStream inputStream = con.getInputStream()) {
while ((bytesRead = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
}

Response<T> response = new Response<>();
response.headers = transformHeaders(con.getHeaderFields());
response.rawPayload = byteArrayOutputStream.toString("UTF-8");
response.event = parseEvent.apply(response.rawPayload);
return response;
}

static Map<String, String> transformHeaders(Map<String, List<String>> headers) {
if (null == headers) {
return null;
}
HashMap<String, String> newMap = new HashMap<>();
headers.forEach((key, value) -> newMap.put(key, String.join(";", value)));
return newMap;
}

public static class Response<T> {
public Map<String, String> headers;
public String rawPayload;
public T event;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,14 @@
import com.sinch.sdk.domains.conversation.models.v1.webhooks.events.smartconversations.MessageInboundSmartConversationRedactionEvent;
import com.sinch.sdk.domains.conversation.models.v1.webhooks.events.smartconversations.SmartConversationsEvent;
import com.sinch.sdk.e2e.Config;
import com.sinch.sdk.e2e.domains.WebhooksHelper;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Instant;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -116,14 +113,15 @@ public class WebhooksEventsSteps {

WebHooksService service;

Map<WebhookTrigger, Response> receivedEvents = new ConcurrentHashMap<>();
Map<WebhookTrigger, WebhooksHelper.Response<ConversationWebhookEvent>> receivedEvents =
new ConcurrentHashMap<>();

Response eventDeliveryReceiptFailedResponse;
Response messageDeliveryReceiptFailedResponse;
Response messageSubmitMediaResponse;
Response messageSubmitTextResponse;
Response smartConversationMediaResponse;
Response smartConversationTextResponse;
WebhooksHelper.Response<ConversationWebhookEvent> eventDeliveryReceiptFailedResponse;
WebhooksHelper.Response<ConversationWebhookEvent> messageDeliveryReceiptFailedResponse;
WebhooksHelper.Response<ConversationWebhookEvent> messageSubmitMediaResponse;
WebhooksHelper.Response<ConversationWebhookEvent> messageSubmitTextResponse;
WebhooksHelper.Response<ConversationWebhookEvent> smartConversationMediaResponse;
WebhooksHelper.Response<ConversationWebhookEvent> smartConversationTextResponse;

@Given("^the Conversation Webhooks handler is available$")
public void handlerAvailable() {
Expand Down Expand Up @@ -173,7 +171,8 @@ public void triggerCONVERSATION_STOP() throws IOException {
@When("^I send a request to trigger a \"EVENT_DELIVERY\" event with a \"FAILED\" status$")
public void triggerEVENT_DELIVERY_FAILED() throws IOException {
eventDeliveryReceiptFailedResponse =
callURL(new URL(WEBHOOKS_PATH + "event-delivery-report/failed"));
WebhooksHelper.callURL(
new URL(WEBHOOKS_PATH + "event-delivery-report/failed"), service::parseEvent);
}

@When("^I send a request to trigger a \"EVENT_DELIVERY\" event with a \"DELIVERED\" status$")
Expand All @@ -189,7 +188,8 @@ public void triggerINBOUND() throws IOException {
@When("^I send a request to trigger a \"MESSAGE_DELIVERY\" event with a \"FAILED\" status$")
public void triggerMESSAGE_DELIVERY_FAILED() throws IOException {
messageDeliveryReceiptFailedResponse =
callURL(new URL(WEBHOOKS_PATH + "message-delivery-report/failed"));
WebhooksHelper.callURL(
new URL(WEBHOOKS_PATH + "message-delivery-report/failed"), service::parseEvent);
}

@When(
Expand All @@ -211,28 +211,36 @@ public void triggerMESSAGE_INBOUND_SMART_CONVERSATION_REDACTION() throws IOExcep

@When("^I send a request to trigger a \"MESSAGE_SUBMIT\" event for a \"media\" message$")
public void triggerMESSAGE_SUBMIT_MEDIA() throws IOException {
messageSubmitMediaResponse = callURL(new URL(WEBHOOKS_PATH + "message-submit/media"));
messageSubmitMediaResponse =
WebhooksHelper.callURL(
new URL(WEBHOOKS_PATH + "message-submit/media"), service::parseEvent);
}

@When("^I send a request to trigger a \"MESSAGE_SUBMIT\" event for a \"text\" message$")
public void triggerMESSAGE_SUBMIT_TEXT() throws IOException {
messageSubmitTextResponse = callURL(new URL(WEBHOOKS_PATH + "message-submit/text"));
messageSubmitTextResponse =
WebhooksHelper.callURL(new URL(WEBHOOKS_PATH + "message-submit/text"), service::parseEvent);
}

@When("^I send a request to trigger a \"SMART_CONVERSATIONS\" event for a \"media\" message$")
public void triggerSMART_CONVERSATIONS_MEDIA() throws IOException {
smartConversationMediaResponse = callURL(new URL(WEBHOOKS_PATH + "smart-conversations/media"));
smartConversationMediaResponse =
WebhooksHelper.callURL(
new URL(WEBHOOKS_PATH + "smart-conversations/media"), service::parseEvent);
}

@When("^I send a request to trigger a \"SMART_CONVERSATIONS\" event for a \"text\" message$")
public void triggerSMART_CONVERSATIONS_TEXT() throws IOException {
smartConversationTextResponse = callURL(new URL(WEBHOOKS_PATH + "smart-conversations/text"));
smartConversationTextResponse =
WebhooksHelper.callURL(
new URL(WEBHOOKS_PATH + "smart-conversations/text"), service::parseEvent);
}

@Then("the header of the Conversation event {string} contains a valid signature")
public void validateHeader(String e2eKeyword) {

Response receivedEvent = receivedEvents.get(WebhookTrigger.from(e2eKeyword));
WebhooksHelper.Response<ConversationWebhookEvent> receivedEvent =
receivedEvents.get(WebhookTrigger.from(e2eKeyword));

if (null != receivedEvent) {
boolean validated =
Expand All @@ -245,7 +253,7 @@ public void validateHeader(String e2eKeyword) {
@Then("the Conversation event describes a {string} event type")
public void triggerResult(String e2eKeyword) {
WebhookTrigger trigger = WebhookTrigger.from(e2eKeyword);
Response receivedEvent = receivedEvents.get(trigger);
WebhooksHelper.Response<ConversationWebhookEvent> receivedEvent = receivedEvents.get(trigger);
if (null != receivedEvent) {
Assertions.assertInstanceOf(expectedClasses.get(trigger), receivedEvent.event);
}
Expand All @@ -256,7 +264,7 @@ public void triggerResult(String e2eKeyword) {
+ " signature")
public void validateEventDeliveryFailureHeader(String e2eKeyword, String status) {

Response receivedEvent = null;
WebhooksHelper.Response<ConversationWebhookEvent> receivedEvent = null;
if (e2eKeyword.equals(WebhookTrigger.EVENT_DELIVERY.value())
&& status.equals(DeliveryStatus.FAILED.value())) {
receivedEvent = eventDeliveryReceiptFailedResponse;
Expand Down Expand Up @@ -359,7 +367,7 @@ public void messageDeliveryFailureResult() {
"the header of the Conversation event {string} for a {string} message contains a valid"
+ " signature")
public void validateMessageSubmitHeader(String e2eKeyword, String messageType) {
Response receivedEvent = null;
WebhooksHelper.Response<ConversationWebhookEvent> receivedEvent = null;
if (e2eKeyword.equals("MESSAGE_SUBMIT") && messageType.equals("media")) {
receivedEvent = messageSubmitMediaResponse;
} else if (e2eKeyword.equals("MESSAGE_SUBMIT") && messageType.equals("text")) {
Expand Down Expand Up @@ -440,49 +448,8 @@ public void messageTypeEventResult(String e2eKeyword, String messageType) {

public void handleRequest(WebhookTrigger trigger) throws IOException {

Response response = callURL(new URL(triggerToURL.get(trigger)));
WebhooksHelper.Response<ConversationWebhookEvent> response =
WebhooksHelper.callURL(new URL(triggerToURL.get(trigger)), service::parseEvent);
receivedEvents.put(trigger, response);
}

Response callURL(URL url) throws IOException {

HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = con.getInputStream().read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
Response response = new Response();
response.headers = transformHeaders(con.getHeaderFields());
response.rawPayload = byteArrayOutputStream.toString("UTF-8");
response.event = service.parseEvent(response.rawPayload);
return response;
}

static Map<String, String> transformHeaders(Map<String, List<String>> headers) {
if (null == headers) {
return null;
}
HashMap<String, String> newMap = new HashMap<>();
headers.forEach((key, value) -> newMap.put(key, concatHeaderValues(value)));
return newMap;
}

static String concatHeaderValues(List<String> values) {
if (null == values) {
return null;
}
return values.stream()
.reduce(null, (previous, current) -> (null != previous ? previous + ";" : "") + current);
}

static class Response {
Map<String, String> headers;
String rawPayload;
ConversationWebhookEvent event;
}
}
Loading
Loading