diff --git a/openvidu-server/pom.xml b/openvidu-server/pom.xml
index ae27db39..74203495 100644
--- a/openvidu-server/pom.xml
+++ b/openvidu-server/pom.xml
@@ -370,6 +370,24 @@
${version.hamcrest}
test
+
+ io.openvidu
+ openvidu-test-browsers
+ ${version.openvidu.test.browsers}
+ test
+
+
+ net.bytebuddy
+ byte-buddy
+
+
+
+
+ org.powermock
+ powermock-api-mockito2
+ ${version.powermock}
+ test
+
diff --git a/openvidu-server/src/test/java/io/openvidu/server/test/integration/WebhookIntegrationTest.java b/openvidu-server/src/test/java/io/openvidu/server/test/integration/WebhookIntegrationTest.java
new file mode 100644
index 00000000..c544f44a
--- /dev/null
+++ b/openvidu-server/src/test/java/io/openvidu/server/test/integration/WebhookIntegrationTest.java
@@ -0,0 +1,219 @@
+package io.openvidu.server.test.integration;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.refEq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.http.StatusLine;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.junit.Assert;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.reflect.Whitebox;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.web.WebAppConfiguration;
+
+import com.google.gson.JsonObject;
+
+import io.openvidu.client.internal.ProtocolElements;
+import io.openvidu.java.client.ConnectionProperties;
+import io.openvidu.server.cdr.CallDetailRecord;
+import io.openvidu.server.core.Participant;
+import io.openvidu.server.core.Session;
+import io.openvidu.server.core.SessionEventsHandler;
+import io.openvidu.server.core.Token;
+import io.openvidu.server.kurento.core.KurentoSessionManager;
+import io.openvidu.server.rest.SessionRestController;
+import io.openvidu.server.rpc.RpcNotificationService;
+import io.openvidu.server.test.integration.config.IntegrationTestConfiguration;
+import io.openvidu.server.utils.GeoLocation;
+import io.openvidu.server.webhook.CDRLoggerWebhook;
+import io.openvidu.server.webhook.HttpWebhookSender;
+import io.openvidu.test.browsers.utils.webhook.CustomWebhook;
+
+/**
+ * @author Pablo Fuente (pablofuenteperez@gmail.com)
+ */
+@SpringBootTest(properties = { "OPENVIDU_WEBHOOK=true", "OPENVIDU_WEBHOOK_ENDPOINT=http://localhost:7777/webhook",
+ "OPENVIDU_WEBHOOK_HEADERS=[]",
+ "OPENVIDU_WEBHOOK_EVENTS=[\"sessionCreated\",\"participantJoined\",\"participantLeft\",\"signalSent\"]" })
+@TestPropertySource(locations = "classpath:integration-test.properties")
+@ContextConfiguration(classes = { IntegrationTestConfiguration.class })
+@WebAppConfiguration
+public class WebhookIntegrationTest {
+
+ private static final Logger log = LoggerFactory.getLogger(WebhookIntegrationTest.class);
+
+ @SpyBean
+ private CallDetailRecord cdr;
+
+ @SpyBean
+ private SessionEventsHandler sessionEventsHandler;
+
+ @SpyBean
+ protected RpcNotificationService rpcNotificationService;
+
+ @Autowired
+ protected SessionRestController sessionRestController;
+
+ @Autowired
+ protected KurentoSessionManager kurentoSessionManager;
+
+ private HttpWebhookSender webhook;
+
+ private void mockWebhookHttpClient(int millisecondsDelayOnResponse) throws ClientProtocolException, IOException {
+ CDRLoggerWebhook cdrLoggerWebhook = (CDRLoggerWebhook) cdr.getLoggers().stream()
+ .filter(logger -> logger instanceof CDRLoggerWebhook).findFirst().get();
+ this.webhook = Whitebox.getInternalState(cdrLoggerWebhook, "webhookSender");
+
+ CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class);
+ StatusLine statusLine = mock(StatusLine.class);
+ when(statusLine.getStatusCode()).thenReturn(200);
+ when(httpResponse.getStatusLine()).thenReturn(statusLine);
+
+ this.setHttpClientDelay(millisecondsDelayOnResponse);
+ }
+
+ private void setHttpClientDelay(int millisecondsDelayOnResponse) throws ClientProtocolException, IOException {
+ HttpClient httpClient = PowerMockito.spy((HttpClient) Whitebox.getInternalState(webhook, "httpClient"));
+ doAnswer(invocationOnMock -> {
+ Thread.sleep(millisecondsDelayOnResponse);
+ return invocationOnMock.callRealMethod();
+ }).when(httpClient).execute(Mockito.any(HttpUriRequest.class));
+ Whitebox.setInternalState(webhook, "httpClient", httpClient);
+ }
+
+ @Test
+ @DisplayName("Webhook event and RPC event should not interfere with each other")
+ void webhookEventAndRpcEventShouldNotInterfereWithEachOtherTest() throws Exception {
+
+ log.info("Webhook event and RPC event should not interfere with each other");
+
+ this.mockWebhookHttpClient(500);
+
+ CountDownLatch initLatch = new CountDownLatch(1);
+ CustomWebhook.main(new String[0], initLatch);
+
+ try {
+
+ if (!initLatch.await(30, TimeUnit.SECONDS)) {
+ Assert.fail("Timeout waiting for webhook springboot app to start");
+ CustomWebhook.shutDown();
+ return;
+ }
+
+ final String sessionId = "WEBHOOK_TEST_SESSION";
+
+ this.sessionRestController.initializeSession(Map.of("customSessionId", sessionId));
+
+ // Webhook event "sessionCreated" is delayed 500 ms
+ // Expected TimeoutException
+ assertThrows(TimeoutException.class, () -> {
+ CustomWebhook.waitForEvent("sessionCreated", 250, TimeUnit.MILLISECONDS);
+ });
+ // Now webhook response for event "sessionCreated" should be received
+ CustomWebhook.waitForEvent("sessionCreated", 750, TimeUnit.MILLISECONDS);
+
+ this.sessionRestController.initializeConnection(sessionId, Map.of());
+
+ Session session = kurentoSessionManager.getSessionWithNotActive(sessionId);
+ Token token = new Token("token", sessionId, new ConnectionProperties.Builder().build(), null);
+ String participantPrivateId = "participantPrivateId";
+ Participant participant = kurentoSessionManager.newParticipant(session, participantPrivateId, token, null,
+ mock(GeoLocation.class), "platform", "finalUserId");
+ kurentoSessionManager.joinRoom(participant, sessionId, 1);
+
+ // Webhook event "participantJoined" is delayed 500 ms
+ // Expected TimeoutException
+ assertThrows(TimeoutException.class, () -> {
+ CustomWebhook.waitForEvent("participantJoined", 250, TimeUnit.MILLISECONDS);
+ });
+
+ // Client should have already received "connectionCreated" RPC response
+ // nonetheless
+ verify(sessionEventsHandler, times(1)).onParticipantJoined(refEq(participant), anyString(), anySet(),
+ anyInt(), refEq(null));
+
+ // Now webhook response for event "participantJoined" should be received
+ CustomWebhook.waitForEvent("participantJoined", 750, TimeUnit.MILLISECONDS);
+
+ setHttpClientDelay(1);
+ // These events will be received immediately
+ this.sessionRestController.signal(Map.of("session", sessionId, "type", "1"));
+ this.sessionRestController.signal(Map.of("session", sessionId, "type", "2"));
+ setHttpClientDelay(500);
+ // This event will be received after a delay
+ this.sessionRestController.signal(Map.of("session", sessionId, "type", "3"));
+ setHttpClientDelay(1);
+ // These events should be received immediately after the delayed one
+ this.sessionRestController.signal(Map.of("session", sessionId, "type", "4"));
+ this.sessionRestController.signal(Map.of("session", sessionId, "type", "5"));
+
+ // RPC signal notification should have already been sent 5 times,
+ // no matter WebHook delays
+ verify(rpcNotificationService, times(5)).sendNotification(refEq(participantPrivateId),
+ refEq(ProtocolElements.PARTICIPANTSENDMESSAGE_METHOD), any());
+
+ // Events received immediately
+ JsonObject signal1 = CustomWebhook.waitForEvent("signalSent", 25, TimeUnit.MILLISECONDS);
+ JsonObject signal2 = CustomWebhook.waitForEvent("signalSent", 25, TimeUnit.MILLISECONDS);
+ // Events not received due to timeout
+ assertThrows(TimeoutException.class, () -> {
+ CustomWebhook.waitForEvent("signalSent", 25, TimeUnit.MILLISECONDS);
+ });
+ assertThrows(TimeoutException.class, () -> {
+ CustomWebhook.waitForEvent("signalSent", 25, TimeUnit.MILLISECONDS);
+ });
+ // Events now received after timeout
+ JsonObject signal3 = CustomWebhook.waitForEvent("signalSent", 500, TimeUnit.MILLISECONDS);
+ JsonObject signal4 = CustomWebhook.waitForEvent("signalSent", 25, TimeUnit.MILLISECONDS);
+ JsonObject signal5 = CustomWebhook.waitForEvent("signalSent", 25, TimeUnit.MILLISECONDS);
+
+ // Order of webhook events should be honored
+ Assert.assertEquals("Wrong signal type", "1", signal1.get("type").getAsString());
+ Assert.assertEquals("Wrong signal type", "2", signal2.get("type").getAsString());
+ Assert.assertEquals("Wrong signal type", "3", signal3.get("type").getAsString());
+ Assert.assertEquals("Wrong signal type", "4", signal4.get("type").getAsString());
+ Assert.assertEquals("Wrong signal type", "5", signal5.get("type").getAsString());
+
+ this.sessionRestController.closeConnection(sessionId, participant.getParticipantPublicId());
+
+ // Webhook is configured to receive "participantLeft" event
+ CustomWebhook.waitForEvent("participantLeft", 25, TimeUnit.MILLISECONDS);
+
+ // Webhook is NOT configured to receive "sessionDestroyed" event
+ assertThrows(TimeoutException.class, () -> {
+ CustomWebhook.waitForEvent("sessionDestroyed", 500, TimeUnit.MILLISECONDS);
+ });
+
+ } finally {
+ CustomWebhook.shutDown();
+ }
+ }
+
+}
diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java
index 1dab8e6b..1be6b3c0 100644
--- a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java
+++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java
@@ -64,11 +64,17 @@ public class CustomWebhook {
CustomWebhook.events.clear();
}
- public synchronized static JsonObject waitForEvent(String eventName, int maxSecondsWait) throws TimeoutException, InterruptedException {
+ public synchronized static JsonObject waitForEvent(String eventName, int maxSecondsWait)
+ throws TimeoutException, InterruptedException {
+ return CustomWebhook.waitForEvent(eventName, maxSecondsWait, TimeUnit.SECONDS);
+ }
+
+ public synchronized static JsonObject waitForEvent(String eventName, int maxWait, TimeUnit timeUnit)
+ throws TimeoutException, InterruptedException {
if (events.get(eventName) == null) {
events.put(eventName, new LinkedBlockingDeque<>());
}
- JsonObject event = CustomWebhook.events.get(eventName).poll(maxSecondsWait, TimeUnit.SECONDS);
+ JsonObject event = CustomWebhook.events.get(eventName).poll(maxWait, timeUnit);
if (event == null) {
throw new TimeoutException("Timeout waiting for Webhook " + eventName);
} else {