diff --git a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java index 26f617c..17217de 100644 --- a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java @@ -70,12 +70,6 @@ public class WAMessageHandler implements MessageHandler { } }; - public WAMessageHandler(String verifyToken, String appSecret, String accessToken) { - this.verifyToken = verifyToken; - this.appSecret = appSecret; - this.accessToken = accessToken; - } - public WAMessageHandler(WAMessengerConfig config) { this.verifyToken = config.verifyToken(); this.accessToken = config.accessToken(); diff --git a/src/test/java/com/meta/cp4m/DummyWebServer.java b/src/test/java/com/meta/cp4m/DummyWebServer.java new file mode 100644 index 0000000..c0621cb --- /dev/null +++ b/src/test/java/com/meta/cp4m/DummyWebServer.java @@ -0,0 +1,87 @@ +/* + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.meta.cp4m; + +import io.javalin.Javalin; +import io.javalin.http.HandlerType; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class DummyWebServer implements AutoCloseable { + private final Javalin javalin; + private final BlockingQueue receivedRequests = new LinkedBlockingDeque<>(); + + private DummyWebServer() { + this.javalin = + Javalin.create() + .addHandler( + HandlerType.GET, + "/", + ctx -> + receivedRequests.put( + new ReceivedRequest( + ctx.path(), + ctx.body(), + ctx.contentType(), + ctx.headerMap(), + ctx.queryParamMap()))) + .addHandler( + HandlerType.POST, + "/", + ctx -> + receivedRequests.put( + new ReceivedRequest( + ctx.path(), + ctx.body(), + ctx.contentType(), + ctx.headerMap(), + ctx.queryParamMap()))) + .start(0); + } + + public static DummyWebServer create() { + return new DummyWebServer(); + } + + public @Nullable ReceivedRequest poll() { + return receivedRequests.poll(); + } + + public @Nullable ReceivedRequest poll(long milliseconds) throws InterruptedException { + return receivedRequests.poll(milliseconds, TimeUnit.MILLISECONDS); + } + + public ReceivedRequest take(long milliseconds) throws InterruptedException { + return Objects.requireNonNull(receivedRequests.poll(milliseconds, TimeUnit.MILLISECONDS)); + } + + public int port() { + return javalin.port(); + } + + public Javalin javalin() { + return javalin; + } + + @Override + public void close() { + javalin.close(); + } + + public record ReceivedRequest( + String path, + String body, + @Nullable String contentType, + Map headerMap, + Map> stringListMap) {} +} diff --git a/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java b/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java index 4c525ba..4802584 100644 --- a/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java +++ b/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java @@ -308,7 +308,7 @@ void inPipeline() throws IOException, URISyntaxException, InterruptedException { // TODO: create test harness Request request = - FBMessageRouteDetailsTest.createMessageRequest(FBMessageRouteDetailsTest.SAMPLE_MESSAGE, runner); + FBMessageHandlerTest.createMessageRequest(FBMessageHandlerTest.SAMPLE_MESSAGE, runner); HttpResponse response = request.execute().returnResponse(); assertThat(response.getCode()).isEqualTo(200); @Nullable OutboundRequest or = HuggingFaceLlamaRequests.poll(500, TimeUnit.MILLISECONDS); diff --git a/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java b/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java index 8f59d3b..d11aee5 100644 --- a/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java +++ b/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java @@ -251,7 +251,7 @@ void inPipeline() throws IOException, URISyntaxException, InterruptedException { // TODO: create test harness Request request = - FBMessageRouteDetailsTest.createMessageRequest(FBMessageRouteDetailsTest.SAMPLE_MESSAGE, runner); + FBMessageHandlerTest.createMessageRequest(FBMessageHandlerTest.SAMPLE_MESSAGE, runner); HttpResponse response = request.execute().returnResponse(); assertThat(response.getCode()).isEqualTo(200); @Nullable OutboundRequest or = openAIRequests.poll(500, TimeUnit.MILLISECONDS); diff --git a/src/test/java/com/meta/cp4m/message/FBMessageRouteDetailsTest.java b/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java similarity index 98% rename from src/test/java/com/meta/cp4m/message/FBMessageRouteDetailsTest.java rename to src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java index 428d72f..7ef044b 100644 --- a/src/test/java/com/meta/cp4m/message/FBMessageRouteDetailsTest.java +++ b/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java @@ -50,7 +50,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -public class FBMessageRouteDetailsTest { +public class FBMessageHandlerTest { /** Example message collected directly from the messenger webhook */ public static final String SAMPLE_MESSAGE = @@ -59,15 +59,19 @@ public class FBMessageRouteDetailsTest { private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String SAMPLE_MESSAGE_HMAC = "sha256=8620d18213fa2612d16117b65168ef97404fa13189528014c5362fec31215985"; - private static JsonNode PARSED_SAMPLE_MESSAGE; - private Javalin app; - private BlockingQueue requests; + public static final JsonNode PARSED_SAMPLE_MESSAGE; - @BeforeAll - static void beforeAll() throws JsonProcessingException { - PARSED_SAMPLE_MESSAGE = MAPPER.readTree(SAMPLE_MESSAGE); + static { + try { + PARSED_SAMPLE_MESSAGE = MAPPER.readTree(SAMPLE_MESSAGE); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } + private Javalin app; + private BlockingQueue requests; + private static HttpResponse getRequest(String path, int port, Map params) throws IOException, URISyntaxException { URIBuilder uriBuilder = diff --git a/src/test/java/com/meta/cp4m/message/HandlerTestUtils.java b/src/test/java/com/meta/cp4m/message/HandlerTestUtils.java new file mode 100644 index 0000000..32b236b --- /dev/null +++ b/src/test/java/com/meta/cp4m/message/HandlerTestUtils.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.meta.cp4m.message; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.meta.cp4m.Identifier; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.function.Function; +import org.apache.hc.client5.http.fluent.Request; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.net.URIBuilder; + +public final class HandlerTestUtils { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + // static class, do not instantiate + private HandlerTestUtils() {} + + public static Function baseURLFactory(String path, int port) { + return identifier -> { + try { + return URIBuilder.localhost().setPort(port).appendPath(path).setScheme("http").build(); + } catch (UnknownHostException | URISyntaxException e) { + throw new RuntimeException(e); + } + }; + } + + public static Function MessageRequestFactory( + Method method, String path, String appSecret, int port) + throws UnknownHostException, URISyntaxException { + Request request = + Request.create( + method, + URIBuilder.localhost().setScheme("http").appendPath(path).setPort(port).build()); + return jn -> { + try { + String body = MAPPER.writeValueAsString(jn); + return request + .bodyString(body, ContentType.APPLICATION_JSON) + .setHeader("X-Hub-Signature-256", "sha256=" + MetaHandlerUtils.hmac(body, appSecret)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }; + } +} diff --git a/src/test/java/com/meta/cp4m/message/MultiServiceTest.java b/src/test/java/com/meta/cp4m/message/MultiServiceTest.java new file mode 100644 index 0000000..694df78 --- /dev/null +++ b/src/test/java/com/meta/cp4m/message/MultiServiceTest.java @@ -0,0 +1,140 @@ +/* + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.meta.cp4m.message; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.meta.cp4m.DummyWebServer; +import com.meta.cp4m.DummyWebServer.ReceivedRequest; +import com.meta.cp4m.Identifier; +import com.meta.cp4m.Service; +import com.meta.cp4m.ServicesRunner; +import com.meta.cp4m.llm.DummyLLMPlugin; +import com.meta.cp4m.store.MemoryStore; +import com.meta.cp4m.store.MemoryStoreConfig; +import java.net.URI; +import java.util.List; +import java.util.function.Function; +import org.apache.hc.client5.http.fluent.Request; +import org.apache.hc.core5.http.Method; +import org.junit.jupiter.api.Test; + +public class MultiServiceTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final String META_PATH = "/meta"; + private final DummyWebServer metaWebServer = DummyWebServer.create(); + + private final Function baseURLFactory = + HandlerTestUtils.baseURLFactory(META_PATH, metaWebServer.port()); + + @Test + void waAndFBTest() throws Exception { + final String path = "/path"; + final String fb1VerifyToken = "fb1VerifyToken"; + final String fb1AppSecret = "fb1AppSecret"; + final String fb1PageAccessToken = "fb1PageAccessToken"; + + MemoryStore fb1Store = MemoryStoreConfig.of(1, 1).toStore(); + FBMessageHandler fb1Handler = + FBMessengerConfig.of(fb1VerifyToken, fb1AppSecret, fb1PageAccessToken) + .toMessageHandler() + .baseURLFactory(baseURLFactory); + DummyLLMPlugin fb1Plugin = new DummyLLMPlugin<>("i'm a fb1 dummy"); + Service fb1Service = new Service<>(fb1Store, fb1Handler, fb1Plugin, path); + + final String fb2VerifyToken = "fb2VerifyToken"; + final String fb2AppSecret = "fb2AppSecret"; + final String fb2PageAccessToken = "fb2PageAccessToken"; + + MemoryStore fb2Store = MemoryStoreConfig.of(1, 1).toStore(); + FBMessageHandler fb2Handler = + FBMessengerConfig.of(fb2VerifyToken, fb2AppSecret, fb2PageAccessToken) + .toMessageHandler() + .baseURLFactory(baseURLFactory); + DummyLLMPlugin fb2Plugin = new DummyLLMPlugin<>("i'm a fb2 dummy"); + Service fb2Service = new Service<>(fb2Store, fb2Handler, fb2Plugin, path); + + final String wa1VerifyToken = "wa1VerifyToken"; + final String wa1AppSecret = "wa1AppSecret"; + final String wa1PageAccessToken = "wa1PageAccessToken"; + MemoryStore wa1Store = MemoryStoreConfig.of(1, 1).toStore(); + WAMessageHandler wa1Handler = + WAMessengerConfig.of(wa1VerifyToken, wa1AppSecret, wa1PageAccessToken) + .toMessageHandler() + .baseUrlFactory(baseURLFactory); + DummyLLMPlugin wa1Plugin = new DummyLLMPlugin<>("i'm a wa1 dummy"); + Service wa1Service = new Service<>(wa1Store, wa1Handler, wa1Plugin, path); + + final String wa2VerifyToken = "wa2VerifyToken"; + final String wa2AppSecret = "wa2AppSecret"; + final String wa2PageAccessToken = "wa2PageAccessToken"; + MemoryStore wa2Store = MemoryStoreConfig.of(1, 1).toStore(); + WAMessageHandler wa2Handler = + WAMessengerConfig.of(wa2VerifyToken, wa2AppSecret, wa2PageAccessToken) + .toMessageHandler() + .baseUrlFactory(baseURLFactory); + DummyLLMPlugin wa2Plugin = new DummyLLMPlugin<>("i'm a wa2 dummy"); + Service wa2Service = new Service<>(wa2Store, wa2Handler, wa2Plugin, path); + + ServicesRunner runner = + ServicesRunner.newInstance() + .service(fb1Service) + .service(fb2Service) + .service(wa1Service) + .service(wa2Service) + .port(0) + .start(); + + // FB1 test + Function fb1RequestFactory = + HandlerTestUtils.MessageRequestFactory(Method.POST, path, fb1AppSecret, runner.port()); + fb1RequestFactory.apply(FBMessageHandlerTest.PARSED_SAMPLE_MESSAGE).execute(); + fb1Plugin.take(500); + ReceivedRequest receivedRequest = metaWebServer.take(500); + assertThat(receivedRequest.path()).isEqualTo(META_PATH); + assertThat(receivedRequest.body()).contains("i'm a fb1 dummy"); + + // FB2 test + Function fb2RequestFactory = + HandlerTestUtils.MessageRequestFactory(Method.POST, path, fb2AppSecret, runner.port()); + fb2RequestFactory.apply(FBMessageHandlerTest.PARSED_SAMPLE_MESSAGE).execute(); + fb2Plugin.take(500); + receivedRequest = metaWebServer.take(500); + assertThat(receivedRequest.path()).isEqualTo(META_PATH); + assertThat(receivedRequest.body()).contains("i'm a fb2 dummy"); + + // WA1 test + Function wa1RequestFactory = + HandlerTestUtils.MessageRequestFactory(Method.POST, path, wa1AppSecret, runner.port()); + wa1RequestFactory.apply(MAPPER.readTree(WAMessageHandlerTest.VALID)).execute(); + wa1Plugin.take(500); + receivedRequest = metaWebServer.take(500); + ReceivedRequest receivedRequest2 = metaWebServer.take(500); + assertThat(receivedRequest.path()).isEqualTo(META_PATH); + assertThat(receivedRequest2.path()).isEqualTo(META_PATH); + assertThat(List.of(receivedRequest, receivedRequest2)) + .satisfiesOnlyOnce(r -> assertThat(r.body()).contains("i'm a wa1 dummy")); + + // WA2 test + Function wa2RequestFactory = + HandlerTestUtils.MessageRequestFactory(Method.POST, path, wa2AppSecret, runner.port()); + wa2RequestFactory.apply(MAPPER.readTree(WAMessageHandlerTest.VALID)).execute(); + wa2Plugin.take(500); + receivedRequest = metaWebServer.take(500); + receivedRequest2 = metaWebServer.take(500); + assertThat(receivedRequest.path()).isEqualTo(META_PATH); + assertThat(receivedRequest2.path()).isEqualTo(META_PATH); + assertThat(List.of(receivedRequest, receivedRequest2)) + .satisfiesOnlyOnce(r -> assertThat(r.body()).contains("i'm a wa2 dummy")); + } +} diff --git a/src/test/java/com/meta/cp4m/message/ServiceTestHarness.java b/src/test/java/com/meta/cp4m/message/ServiceTestHarness.java index 58dad01..b126949 100644 --- a/src/test/java/com/meta/cp4m/message/ServiceTestHarness.java +++ b/src/test/java/com/meta/cp4m/message/ServiceTestHarness.java @@ -8,26 +8,21 @@ package com.meta.cp4m.message; +import com.meta.cp4m.DummyWebServer; +import com.meta.cp4m.DummyWebServer.ReceivedRequest; import com.meta.cp4m.Service; import com.meta.cp4m.ServicesRunner; import com.meta.cp4m.llm.DummyLLMPlugin; import com.meta.cp4m.llm.LLMPlugin; import com.meta.cp4m.store.ChatStore; import com.meta.cp4m.store.MemoryStoreConfig; -import io.javalin.Javalin; -import io.javalin.http.HandlerType; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; -import java.util.Map; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; import org.apache.hc.client5.http.fluent.Request; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.net.URIBuilder; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.common.reflection.qual.NewInstance; import org.checkerframework.common.returnsreceiver.qual.This; public class ServiceTestHarness { @@ -36,13 +31,12 @@ public class ServiceTestHarness { private static final String APP_SECRET = "test_app_secret"; private static final String SERVICE_PATH = "/testservice"; private static final String WEBSERVER_PATH = "/testserver"; - private final BlockingQueue receivedRequests = new ArrayBlockingQueue<>(1000); private final ChatStore chatStore; private final MessageHandler handler; private final LLMPlugin llmPlugin; private final Service service; private final ServicesRunner runner; - private final Javalin javalin; + private final DummyWebServer dummyWebServer = DummyWebServer.create(); private ServiceTestHarness( ChatStore chatStore, MessageHandler handler, LLMPlugin llmPlugin) { @@ -51,7 +45,6 @@ private ServiceTestHarness( this.llmPlugin = llmPlugin; this.service = new Service<>(chatStore, handler, llmPlugin, SERVICE_PATH); this.runner = ServicesRunner.newInstance().service(service); - javalin = newJavalin(); } public static ServiceTestHarness newWAServiceTestHarness() { @@ -64,46 +57,6 @@ public static ServiceTestHarness newWAServiceTestHarness() { return harness; } - public Service service() { - return service; - } - - public ServicesRunner runner() { - return runner; - } - - public Javalin javalin() { - return javalin; - } - - private Javalin newJavalin() { - Javalin javalin = Javalin.create(); - javalin - .addHandler( - HandlerType.GET, - "/", - ctx -> - receivedRequests.put( - new ReceivedRequest( - ctx.path(), - ctx.body(), - ctx.contentType(), - ctx.headerMap(), - ctx.queryParamMap()))) - .addHandler( - HandlerType.POST, - "/", - ctx -> - receivedRequests.put( - new ReceivedRequest( - ctx.path(), - ctx.body(), - ctx.contentType(), - ctx.headerMap(), - ctx.queryParamMap()))); - return javalin; - } - public Request post() { return Request.post(serviceURI()); } @@ -146,23 +99,14 @@ public URI serviceURI() { } } - public @NewInstance ServiceTestHarness withLLMPlugin(LLMPlugin plugin) { - return new ServiceTestHarness<>(chatStore, handler, plugin); - } - - public @NewInstance ServiceTestHarness withChatStore(ChatStore chatStore) { - return new ServiceTestHarness<>(chatStore, handler, llmPlugin); - } - public @This ServiceTestHarness start() { - javalin.start(0); runner.port(0).start(); return this; } public @This ServiceTestHarness stop() { runner.close(); - javalin.close(); + dummyWebServer.close(); return this; } @@ -203,22 +147,10 @@ public int servicePort() { } public int webserverPort() { - return javalin.port(); + return dummyWebServer.port(); } - public ServiceTestHarness.@Nullable ReceivedRequest pollWebserver(long milliseconds) - throws InterruptedException { - return receivedRequests.poll(milliseconds, TimeUnit.MILLISECONDS); + public @Nullable ReceivedRequest pollWebserver(long milliseconds) throws InterruptedException { + return dummyWebServer.poll(milliseconds); } - - public ServiceTestHarness.@Nullable ReceivedRequest pollWebserver() { - return receivedRequests.poll(); - } - - public record ReceivedRequest( - String path, - String body, - @Nullable String contentType, - Map headerMap, - Map> stringListMap) {} } diff --git a/src/test/java/com/meta/cp4m/message/WAMessageRouteDetailsTest.java b/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java similarity index 92% rename from src/test/java/com/meta/cp4m/message/WAMessageRouteDetailsTest.java rename to src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java index 14fbb29..ed4b5a2 100644 --- a/src/test/java/com/meta/cp4m/message/WAMessageRouteDetailsTest.java +++ b/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java @@ -10,11 +10,9 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.meta.cp4m.DummyWebServer.ReceivedRequest; import com.meta.cp4m.Identifier; -import com.meta.cp4m.message.ServiceTestHarness.ReceivedRequest; import com.meta.cp4m.message.webhook.whatsapp.Utils; import java.io.IOException; import java.util.Collection; @@ -25,7 +23,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class WAMessageRouteDetailsTest { +class WAMessageHandlerTest { static final String VALID = """ @@ -101,13 +99,9 @@ class WAMessageRouteDetailsTest { } """; private static final JsonMapper MAPPER = Utils.JSON_MAPPER; - private final ObjectNode validNode = (ObjectNode) MAPPER.readTree(VALID); - private final ServiceTestHarness harness = ServiceTestHarness.newWAServiceTestHarness(); - WAMessageRouteDetailsTest() throws JsonProcessingException {} - @BeforeEach void setUp() { harness.start();