From e60ac50d266c2f0c5f06871eb1e912c7a85e8e3b Mon Sep 17 00:00:00 2001 From: pabloFuente Date: Sun, 14 Nov 2021 23:16:54 +0100 Subject: [PATCH] WebhookIntegrationTest --- openvidu-server/pom.xml | 18 ++ .../integration/WebhookIntegrationTest.java | 219 ++++++++++++++++++ .../browsers/utils/webhook/CustomWebhook.java | 10 +- 3 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 openvidu-server/src/test/java/io/openvidu/server/test/integration/WebhookIntegrationTest.java 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 {