diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotState.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotState.java index 100a6fce..807a4336 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotState.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotState.java @@ -81,7 +81,7 @@ public abstract class BotState implements PropertyManager { if (force || cachedState == null || cachedState.getState() == null) { return storage.read(new String[]{storageKey}) .thenApply(val -> { - turnContext.getTurnState().put(contextServiceKey, new CachedBotState((Map)val.get(storageKey))); + turnContext.getTurnState().replace(contextServiceKey, new CachedBotState((Map)val.get(storageKey))); return null; }); } @@ -141,7 +141,7 @@ public abstract class BotState implements PropertyManager { throw new IllegalArgumentException("turnContext cannot be null"); } - turnContext.getTurnState().put(contextServiceKey, new CachedBotState()); + turnContext.getTurnState().replace(contextServiceKey, new CachedBotState()); return CompletableFuture.completedFuture(null); } @@ -179,7 +179,7 @@ public abstract class BotState implements PropertyManager { throw new IllegalArgumentException("turnContext cannot be null"); } - String stateKey = getClass().getName(); + String stateKey = getClass().getSimpleName(); CachedBotState cachedState = turnContext.getTurnState().get(stateKey); return new ObjectMapper().valueToTree(cachedState.state); } diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java index b5649d7d..212c8b7e 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextStateCollection.java @@ -11,13 +11,15 @@ import java.util.Map; /** * Represents a set of collection of services associated with the {@link TurnContext}. */ -public class TurnContextStateCollection extends HashMap implements AutoCloseable { +public class TurnContextStateCollection implements AutoCloseable { + private Map state = new HashMap<>(); + public T get(String key) throws IllegalArgumentException { if (key == null) { throw new IllegalArgumentException("key"); } - Object service = super.get(key); + Object service = state.get(key); try { T result = (T) service; } catch (ClassCastException e) { @@ -30,11 +32,12 @@ public class TurnContextStateCollection extends HashMap implemen /** * Get a service by type using its full type name as the key. * - * @param type The type of service to be retrieved. + * @param type The type of service to be retrieved. This will use the value returned + * by Class.getSimpleName as the key. * @return The service stored under the specified key. */ public T get(Class type) throws IllegalArgumentException { - return get(type.getName()); + return get(type.getSimpleName()); } public void add(String key, T value) throws IllegalArgumentException { @@ -43,21 +46,36 @@ public class TurnContextStateCollection extends HashMap implemen } if (value == null) { - throw new IllegalArgumentException("service"); + throw new IllegalArgumentException("value"); } - if (containsKey(key)) + if (state.containsKey(key)) { throw new IllegalArgumentException(String.format("Key %s already exists", key)); - put(key, value); + } + + state.put(key, value); } /** - * Add a service using its full type name as the key. + * Add a service using its type name ({@link Class#getSimpleName()} as the key. * * @param value The service to add. */ public void add(T value) throws IllegalArgumentException { - add(value.getClass().getName(), value); + if (value == null) { + throw new IllegalArgumentException("value"); + } + + add(value.getClass().getSimpleName(), value); + } + + public void remove(String key) { + state.remove(key); + } + + public void replace(String key, Object value) { + state.remove(key); + add(key, value); } @Override @@ -71,7 +89,7 @@ public class TurnContextStateCollection extends HashMap implemen @Override public void close() throws Exception { - for (Map.Entry entry : entrySet()) { + for (Map.Entry entry : state.entrySet()) { if (entry.getValue() instanceof AutoCloseable) { if (entry.getValue() instanceof ConnectorClient) { continue; diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionMiddleware.java index 9db6de41..61b07311 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionMiddleware.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InspectionMiddleware.java @@ -11,7 +11,6 @@ import com.microsoft.bot.schema.Activity; import com.microsoft.bot.schema.ActivityTypes; import com.microsoft.bot.schema.ConversationReference; import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; @@ -31,16 +30,14 @@ public class InspectionMiddleware extends InterceptionMiddleware { this(withInspectionState, null, null, - null, - LoggerFactory.getLogger(InspectionMiddleware.class)); + null); } public InspectionMiddleware(InspectionState withInspectionState, UserState withUserState, ConversationState withConversationState, - MicrosoftAppCredentials withCredentials, - Logger withLogger) { - super(withLogger); + MicrosoftAppCredentials withCredentials) { + super(LoggerFactory.getLogger(InspectionMiddleware.class)); inspectionState = withInspectionState; userState = withUserState; @@ -49,7 +46,7 @@ public class InspectionMiddleware extends InterceptionMiddleware { } public CompletableFuture processCommand(TurnContext turnContext) { - if (!StringUtils.equals(turnContext.getActivity().getType(), ActivityTypes.MESSAGE) + if (!turnContext.getActivity().isType(ActivityTypes.MESSAGE) || StringUtils.isEmpty(turnContext.getActivity().getText())) { return CompletableFuture.completedFuture(false); @@ -58,7 +55,7 @@ public class InspectionMiddleware extends InterceptionMiddleware { String text = Activity.removeRecipientMentionImmutable(turnContext.getActivity()); String[] command = text.split(" "); - if (command.length > 1 && StringUtils.equals(command[1], COMMAND)) { + if (command.length > 1 && StringUtils.equals(command[0], COMMAND)) { if (command.length == 2 && StringUtils.equals(command[1], "open")) { return processOpenCommand(turnContext) .thenApply((result) -> true); @@ -76,14 +73,25 @@ public class InspectionMiddleware extends InterceptionMiddleware { @Override protected CompletableFuture inbound(TurnContext turnContext, Activity activity) { return processCommand(turnContext) - .thenCombine(findSession(turnContext), (processResult, session) -> { - if (session != null) { - if (invokeSend(turnContext, session, activity).join()) { - return new Intercept(true, true); - } + .thenCompose(processResult -> { + if (processResult) { + return CompletableFuture.completedFuture(new Intercept(false, false)); } - return new Intercept(true, false); + return findSession(turnContext) + .thenCompose(session -> { + if (session == null) { + return CompletableFuture.completedFuture(new Intercept(true, false)); + } + + return invokeSend(turnContext, session, activity) + .thenCompose(invokeResult -> { + if (invokeResult) { + return CompletableFuture.completedFuture(new Intercept(true, true)); + } + return CompletableFuture.completedFuture(new Intercept(true, false)); + }); + }); }); } @@ -108,30 +116,34 @@ public class InspectionMiddleware extends InterceptionMiddleware { @Override protected CompletableFuture traceState(TurnContext turnContext) { return findSession(turnContext) - .thenAccept(session -> { - if (session != null) { - CompletableFuture userLoad = userState == null - ? CompletableFuture.completedFuture(null) - : userState.load(turnContext); - - CompletableFuture conversationLoad = conversationState == null - ? CompletableFuture.completedFuture(null) - : conversationState.load(turnContext); - - CompletableFuture.allOf(userLoad, conversationLoad).join(); - - ObjectNode botState = JsonNodeFactory.instance.objectNode(); - if (userState != null) { - botState.set("userState", userState.get(turnContext)); - } - - if (conversationState != null) { - botState.set("conversationState", conversationState.get(turnContext)); - } - - invokeSend(turnContext, session, InspectionActivityExtensions.traceActivity(botState)).join(); + .thenCompose(session -> { + if (session == null) { + return CompletableFuture.completedFuture(null); } - }); + + CompletableFuture userLoad = userState == null + ? CompletableFuture.completedFuture(null) + : userState.load(turnContext); + + CompletableFuture conversationLoad = conversationState == null + ? CompletableFuture.completedFuture(null) + : conversationState.load(turnContext); + + return CompletableFuture.allOf(userLoad, conversationLoad) + .thenCompose(loadResult -> { + ObjectNode botState = JsonNodeFactory.instance.objectNode(); + if (userState != null) { + botState.set("userState", userState.get(turnContext)); + } + + if (conversationState != null) { + botState.set("conversationState", conversationState.get(turnContext)); + } + + return invokeSend(turnContext, session, InspectionActivityExtensions.traceActivity(botState)) + .thenCompose(invokeResult -> CompletableFuture.completedFuture(null)); + }); + }); } private CompletableFuture processOpenCommand(TurnContext turnContext) { @@ -139,16 +151,14 @@ public class InspectionMiddleware extends InterceptionMiddleware { inspectionState.createProperty(InspectionSessionsByStatus.class.getName()); return accessor.get(turnContext, InspectionSessionsByStatus::new) - .thenAccept(result -> { + .thenCompose(result -> { InspectionSessionsByStatus sessions = (InspectionSessionsByStatus) result; String sessionId = openCommand(sessions, turnContext.getActivity().getConversationReference()); String command = String.format("%s attach %s", COMMAND, sessionId); - turnContext.sendActivity(InspectionActivityExtensions.makeCommandActivity(command)).join(); + return turnContext.sendActivity(InspectionActivityExtensions.makeCommandActivity(command)); }) - .thenRun(() -> { - inspectionState.saveChanges(turnContext); - }); + .thenCompose(resourceResponse -> inspectionState.saveChanges(turnContext)); } private CompletableFuture processAttachCommand(TurnContext turnContext, String sessionId) { @@ -156,20 +166,16 @@ public class InspectionMiddleware extends InterceptionMiddleware { inspectionState.createProperty(InspectionSessionsByStatus.class.getName()); return accessor.get(turnContext, InspectionSessionsByStatus::new) - .thenAccept(result -> { - InspectionSessionsByStatus sessions = (InspectionSessionsByStatus) result; - + .thenCompose(sessions -> { if (attachCommand(turnContext.getActivity().getConversation().getId(), sessions, sessionId)) { - turnContext.sendActivity(MessageFactory.text( - "Attached to session, all traffic is being replicated for inspection.")).join(); + return turnContext.sendActivity(MessageFactory.text( + "Attached to session, all traffic is being replicated for inspection.")); } else { - turnContext.sendActivity(MessageFactory.text( - String.format("Open session with id %s does not exist.", sessionId))).join(); + return turnContext.sendActivity(MessageFactory.text( + String.format("Open session with id %s does not exist.", sessionId))); } }) - .thenRun(() -> { - inspectionState.saveChanges(turnContext); - }); + .thenCompose(resourceResponse -> inspectionState.saveChanges(turnContext)); } private String openCommand(InspectionSessionsByStatus sessions, ConversationReference conversationReference) { @@ -189,6 +195,10 @@ public class InspectionMiddleware extends InterceptionMiddleware { return true; } + protected InspectionSession createSession(ConversationReference reference, MicrosoftAppCredentials credentials) { + return new InspectionSession(reference, credentials); + } + private CompletableFuture findSession(TurnContext turnContext) { StatePropertyAccessor accessor = inspectionState.createProperty(InspectionSessionsByStatus.class.getName()); @@ -201,7 +211,7 @@ public class InspectionMiddleware extends InterceptionMiddleware { .get(turnContext.getActivity().getConversation().getId()); if (reference != null) { - return new InspectionSession(reference, credentials, getLogger()); + return createSession(reference, credentials); } return null; @@ -213,13 +223,13 @@ public class InspectionMiddleware extends InterceptionMiddleware { Activity activity) { return session.send(activity) - .thenApply(result -> { + .thenCompose(result -> { if (result) { - return true; + return CompletableFuture.completedFuture(true); } - cleanupSession(turnContext).join(); - return false; + return cleanupSession(turnContext) + .thenCompose(cleanupResult -> CompletableFuture.completedFuture(false)); }); } diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InterceptionMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InterceptionMiddleware.java index e6d44b8d..ca4b5108 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InterceptionMiddleware.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/inspection/InterceptionMiddleware.java @@ -46,13 +46,31 @@ public abstract class InterceptionMiddleware implements Middleware { turnContext.onSendActivities((sendContext, sendActivities, sendNext) -> { List traceActivities = sendActivities.stream() .map(a -> - InspectionActivityExtensions.traceActivity(a, "SentActivity", "Sent Activity")) + InspectionActivityExtensions.traceActivity(a, + "SentActivity", "Sent Activity")) .collect(Collectors.toList()); return invokeOutbound(sendContext, traceActivities) .thenCompose(response -> { return sendNext.get(); }); }); + + turnContext.onUpdateActivity((updateContext, updateActivity, updateNext) -> { + Activity traceActivity = InspectionActivityExtensions.traceActivity(updateActivity, + "MessageUpdate", "Message Update"); + return invokeOutbound(turnContext, traceActivity) + .thenCompose(response -> { + return updateNext.get(); + }); + }); + + turnContext.onDeleteActivity((deleteContext, deleteReference, deleteNext) -> { + Activity traceActivity = InspectionActivityExtensions.traceActivity(deleteReference); + return invokeOutbound(turnContext, traceActivity) + .thenCompose(response -> { + return deleteNext.get(); + }); + }); } if (intercept.shouldForwardToApplication) { @@ -68,7 +86,7 @@ public abstract class InterceptionMiddleware implements Middleware { return invokeTraceState(turnContext); } - return null; + return CompletableFuture.completedFuture(null); }); } @@ -89,9 +107,9 @@ public abstract class InterceptionMiddleware implements Middleware { private CompletableFuture invokeOutbound(TurnContext turnContext, List traceActivities) { return outbound(turnContext, traceActivities) .exceptionally(exception -> { - logger.warn("Exception in outbound interception {}", exception.getMessage()); - return null; - }); + logger.warn("Exception in outbound interception {}", exception.getMessage()); + return null; + }); } private CompletableFuture invokeOutbound(TurnContext turnContext, Activity activity) { @@ -109,8 +127,8 @@ public abstract class InterceptionMiddleware implements Middleware { private CompletableFuture invokeTraceException(TurnContext turnContext, Activity traceActivity) { return outbound(turnContext, Collections.singletonList(Activity.createContactRelationUpdateActivity())) .exceptionally(exception -> { - logger.warn("Exception in exception interception {}", exception.getMessage()); - return null; - }); + logger.warn("Exception in exception interception {}", exception.getMessage()); + return null; + }); } } diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/InspectionTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/InspectionTests.java new file mode 100644 index 00000000..55e84aee --- /dev/null +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/InspectionTests.java @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.builder; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.microsoft.bot.builder.adapters.TestAdapter; +import com.microsoft.bot.builder.inspection.InspectionMiddleware; +import com.microsoft.bot.builder.inspection.InspectionSession; +import com.microsoft.bot.builder.inspection.InspectionState; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.Entity; +import org.junit.Assert; +import org.junit.Test; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class InspectionTests { + @Test + public void ScenarioWithInspectionMiddlewarePassthrough() { + InspectionState inspectionState = new InspectionState(new MemoryStorage()); + InspectionMiddleware inspectionMiddleware = new InspectionMiddleware(inspectionState); + + TestAdapter adapter = new TestAdapter() + .use(inspectionMiddleware); + + Activity inboundActivity = MessageFactory.text("hello"); + + adapter.processActivity(inboundActivity, turnContext -> { + turnContext.sendActivity(MessageFactory.text("hi")).join(); + return CompletableFuture.completedFuture(null); + }).join(); + + Activity outboundActivity = adapter.activeQueue().poll(); + Assert.assertEquals("hi", outboundActivity.getText()); + } + + @Test + public void ScenarioWithInspectionMiddlewareOpenAttach() throws IOException { + // any bot state should be returned as trace messages per turn + MemoryStorage storage = new MemoryStorage(); + InspectionState inspectionState = new InspectionState(storage); + UserState userState = new UserState(storage); + ConversationState conversationState = new ConversationState(storage); + + TestInspectionMiddleware inspectionMiddleware = new TestInspectionMiddleware( + inspectionState, + userState, + conversationState, + null); + + // (1) send the /INSPECT open command from the emulator to the middleware + Activity openActivity = MessageFactory.text("/INSPECT open"); + + TestAdapter inspectionAdapter = new TestAdapter(Channels.TEST); + inspectionAdapter.processActivity(openActivity, turnContext -> { + inspectionMiddleware.processCommand(turnContext).join(); + return CompletableFuture.completedFuture(null); + }).join(); + + Activity inspectionOpenResultActivity = inspectionAdapter.activeQueue().poll(); + + // (2) send the resulting /INSPECT attach command from the channel to the middleware + TestAdapter applicationAdapter = new TestAdapter(Channels.TEST); + applicationAdapter.use(inspectionMiddleware); + + String attachCommand = inspectionOpenResultActivity.getValue().toString(); + + applicationAdapter.processActivity(MessageFactory.text(attachCommand), turnContext -> { + // nothing happens - just attach the inspector + return CompletableFuture.completedFuture(null); + }).join(); + + Activity attachResponse = applicationAdapter.activeQueue().poll(); + + // (3) send an application messaage from the channel, it should get the reply and then so should the emulator http endpioint + applicationAdapter.processActivity(MessageFactory.text("hi"), turnContext -> { + turnContext.sendActivity(MessageFactory.text("echo: " + turnContext.getActivity().getText())).join(); + + userState.createProperty("x").get(turnContext, Scratch::new).join().setProperty("hello"); + conversationState.createProperty("y").get(turnContext, Scratch::new).join().setProperty("world"); + + userState.saveChanges(turnContext).join(); + conversationState.saveChanges(turnContext).join(); + + return CompletableFuture.completedFuture(null); + }).join(); + + Activity outboundActivity = applicationAdapter.activeQueue().poll(); + Assert.assertEquals("echo: hi", outboundActivity.getText()); + Assert.assertEquals(3, inspectionMiddleware.recordingSession.requests.size()); + + ObjectMapper mapper = new ObjectMapper(); + mapper.findAndRegisterModules(); + + JsonNode inboundTrace = mapper.readTree(inspectionMiddleware.recordingSession.requests.get(0)); + Assert.assertEquals("trace", inboundTrace.get("type").textValue()); + Assert.assertEquals("ReceivedActivity", inboundTrace.get("name").textValue()); + Assert.assertEquals("message", inboundTrace.get("value").get("type").textValue()); + Assert.assertEquals("hi", inboundTrace.get("value").get("text").textValue()); + + JsonNode outboundTrace = mapper.readTree(inspectionMiddleware.recordingSession.requests.get(1)); + Assert.assertEquals("trace", outboundTrace.get("type").textValue()); + Assert.assertEquals("SentActivity", outboundTrace.get("name").textValue()); + Assert.assertEquals("message", outboundTrace.get("value").get("type").textValue()); + Assert.assertEquals("echo: hi", outboundTrace.get("value").get("text").textValue()); + + JsonNode stateTrace = mapper.readTree(inspectionMiddleware.recordingSession.requests.get(2)); + Assert.assertEquals("trace", stateTrace.get("type").textValue()); + Assert.assertEquals("BotState", stateTrace.get("name").textValue()); + Assert.assertEquals("hello", stateTrace.get("value").get("userState").get("x").get("property").textValue()); + Assert.assertEquals("world", stateTrace.get("value").get("conversationState").get("y").get("property").textValue()); + } + + @Test + public void ScenarioWithInspectionMiddlewareOpenAttachWithMention() throws IOException { + // any bot state should be returned as trace messages per turn + MemoryStorage storage = new MemoryStorage(); + InspectionState inspectionState = new InspectionState(storage); + UserState userState = new UserState(storage); + ConversationState conversationState = new ConversationState(storage); + + TestInspectionMiddleware inspectionMiddleware = new TestInspectionMiddleware( + inspectionState, + userState, + conversationState, + null); + + // (1) send the /INSPECT open command from the emulator to the middleware + Activity openActivity = MessageFactory.text("/INSPECT open"); + + TestAdapter inspectionAdapter = new TestAdapter(Channels.TEST); + inspectionAdapter.processActivity(openActivity, turnContext -> { + inspectionMiddleware.processCommand(turnContext).join(); + return CompletableFuture.completedFuture(null); + }).join(); + + Activity inspectionOpenResultActivity = inspectionAdapter.activeQueue().poll(); + + // (2) send the resulting /INSPECT attach command from the channel to the middleware + TestAdapter applicationAdapter = new TestAdapter(Channels.TEST); + applicationAdapter.use(inspectionMiddleware); + + // some channels - for example Microsoft Teams - adds an @ mention to the text - this should be taken into account when evaluating the INSPECT + String recipientId = "bot"; + String attachCommand = "" + recipientId + " " + inspectionOpenResultActivity.getValue(); + Activity attachActivity = MessageFactory.text(attachCommand); + attachActivity.getEntities().add(new Entity() {{ + setType("mention"); + getProperties().put("text", JsonNodeFactory.instance.textNode("" + recipientId + "")); + getProperties().put("mentioned", JsonNodeFactory.instance.objectNode().put("id", "bot")); + }}); + + applicationAdapter.processActivity(attachActivity, turnContext -> { + // nothing happens - just attach the inspector + return CompletableFuture.completedFuture(null); + }).join(); + + Activity attachResponse = applicationAdapter.activeQueue().poll(); + + // (3) send an application messaage from the channel, it should get the reply and then so should the emulator http endpioint + applicationAdapter.processActivity(MessageFactory.text("hi"), turnContext -> { + turnContext.sendActivity(MessageFactory.text("echo: " + turnContext.getActivity().getText())).join(); + + userState.createProperty("x").get(turnContext, Scratch::new).join().setProperty("hello"); + conversationState.createProperty("y").get(turnContext, Scratch::new).join().setProperty("world"); + + userState.saveChanges(turnContext).join(); + conversationState.saveChanges(turnContext).join(); + + return CompletableFuture.completedFuture(null); + }).join(); + + Activity outboundActivity = applicationAdapter.activeQueue().poll(); + Assert.assertEquals("echo: hi", outboundActivity.getText()); + Assert.assertEquals(3, inspectionMiddleware.recordingSession.requests.size()); + + ObjectMapper mapper = new ObjectMapper(); + mapper.findAndRegisterModules(); + + JsonNode inboundTrace = mapper.readTree(inspectionMiddleware.recordingSession.requests.get(0)); + Assert.assertEquals("trace", inboundTrace.get("type").textValue()); + Assert.assertEquals("ReceivedActivity", inboundTrace.get("name").textValue()); + Assert.assertEquals("message", inboundTrace.get("value").get("type").textValue()); + Assert.assertEquals("hi", inboundTrace.get("value").get("text").textValue()); + + JsonNode outboundTrace = mapper.readTree(inspectionMiddleware.recordingSession.requests.get(1)); + Assert.assertEquals("trace", outboundTrace.get("type").textValue()); + Assert.assertEquals("SentActivity", outboundTrace.get("name").textValue()); + Assert.assertEquals("message", outboundTrace.get("value").get("type").textValue()); + Assert.assertEquals("echo: hi", outboundTrace.get("value").get("text").textValue()); + + JsonNode stateTrace = mapper.readTree(inspectionMiddleware.recordingSession.requests.get(2)); + Assert.assertEquals("trace", stateTrace.get("type").textValue()); + Assert.assertEquals("BotState", stateTrace.get("name").textValue()); + Assert.assertEquals("hello", stateTrace.get("value").get("userState").get("x").get("property").textValue()); + Assert.assertEquals("world", stateTrace.get("value").get("conversationState").get("y").get("property").textValue()); + } + + // We can't currently supply a custom httpclient like dotnet. So instead, these test differ from dotnet by + // supplying a custom InspectionSession that records what is sent through it. + private static class TestInspectionMiddleware extends InspectionMiddleware { + public RecordingInspectionSession recordingSession = null; + + public TestInspectionMiddleware(InspectionState withInspectionState) { + super(withInspectionState); + } + + public TestInspectionMiddleware(InspectionState withInspectionState, UserState withUserState, ConversationState withConversationState, MicrosoftAppCredentials withCredentials) { + super(withInspectionState, withUserState, withConversationState, withCredentials); + } + + @Override + protected InspectionSession createSession(ConversationReference reference, MicrosoftAppCredentials credentials) { + if (recordingSession == null) { + recordingSession = new RecordingInspectionSession(reference, credentials); + } + return recordingSession; + } + } + + private static class RecordingInspectionSession extends InspectionSession { + private List requests = new ArrayList<>(); + ObjectMapper mapper = new ObjectMapper(); + + public RecordingInspectionSession(ConversationReference withConversationReference, MicrosoftAppCredentials withCredentials) { + super(withConversationReference, withCredentials); + mapper.findAndRegisterModules(); + } + + public RecordingInspectionSession(ConversationReference withConversationReference, MicrosoftAppCredentials withCredentials, Logger withLogger) { + super(withConversationReference, withCredentials, withLogger); + mapper.findAndRegisterModules(); + } + + public List getRequests() { + return requests; + } + + @Override + public CompletableFuture send(Activity activity) { + try { + requests.add(mapper.writeValueAsString(activity)); + } catch (Throwable t) { + //noop + } + + return CompletableFuture.completedFuture(true); + } + } + + private static class Scratch { + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + + private String property; + } +} diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java index 61c53568..2c0533b0 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java @@ -19,9 +19,28 @@ public class TestAdapter extends BotAdapter { private ConversationReference conversationReference; public TestAdapter() { - this(null); + this((ConversationReference) null); } + public TestAdapter(String channelId) { + setConversationReference(new ConversationReference() {{ + setChannelId(channelId); + setServiceUrl("https://test.com"); + setUser(new ChannelAccount() {{ + setId("user1"); + setName("User1"); + }}); + setBot(new ChannelAccount() {{ + setId("bot"); + setName("Bot"); + }}); + setConversation(new ConversationAccount() {{ + setIsGroup(false); + setConversationType("convo1"); + setId("Conversation1"); + }}); + }}); + } public TestAdapter(ConversationReference reference) { if (reference != null) { diff --git a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ConversationReference.java b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ConversationReference.java index 515ef543..30ed3d0a 100644 --- a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ConversationReference.java +++ b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ConversationReference.java @@ -8,6 +8,7 @@ package com.microsoft.bot.schema; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -76,6 +77,7 @@ public class ConversationReference { /** * Creates {@link Activity} from conversation reference as it is posted to bot. */ + @JsonIgnore public Activity getContinuationActivity() { Activity activity = Activity.createEventActivity(); activity.setName("ContinueConversation");