Skip to content

Commit

Permalink
Add integration test tool for Java BDK Bot (#753)
Browse files Browse the repository at this point in the history
  • Loading branch information
yinan-symphony committed Mar 30, 2023
1 parent 3987ce3 commit e54e61b
Show file tree
Hide file tree
Showing 12 changed files with 451 additions and 40 deletions.
23 changes: 1 addition & 22 deletions allow-list.xml
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
<suppress>
<notes><![CDATA[
Testing false positive, transitive dependency from mockserver-netty
]]></notes>
<gav>org.apache.commons:commons-text:1.9</gav>
<cve>CVE-2022-42889</cve>
</suppress>
<suppress>
<notes><![CDATA[
No fix available
Expand Down Expand Up @@ -50,16 +43,9 @@
Testing false positives by suppressing a CVE
https://github.com/spring-projects/spring-framework/issues/24434 (Do not expose HttpInvoker)
]]></notes>
<gav>org.springframework:spring-web:5.3.25</gav>
<gav>org.springframework:spring-web:5.3.26</gav>
<cve>CVE-2016-1000027</cve>
</suppress>
<suppress>
<notes><![CDATA[
Migrate later with spring 3.0
]]></notes>
<gav>org.springframework:spring-webmvc:5.3.25</gav>
<cve>CVE-2023-20860</cve>
</suppress>
<suppress base="true">
<notes><![CDATA[
FP per issue #5121 - fix for commons
Expand All @@ -81,11 +67,4 @@
<gav>net.minidev:json-smart:2.4.8</gav>
<cve>CVE-2023-1370</cve>
</suppress>
<suppress>
<notes><![CDATA[
Need to upgrade to spring 3.x.x, which requires java 17
]]></notes>
<gav>org.springframework:spring-expression:5.3.25</gav>
<cve>CVE-2023-20861</cve>
</suppress>
</suppressions>
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id "io.codearte.nexus-staging" version "0.22.0"
id "org.owasp.dependencycheck" version "8.1.2"
id "org.owasp.dependencycheck" version "8.2.1"
}

ext.projectVersion = '2.12.0-SNAPSHOT'
Expand Down
136 changes: 120 additions & 16 deletions docs/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,113 @@
This guide provides information to Symphony BDK Bot developers about how using Symphony BDK Test to build the
integration tests for the Bots.

> **Note**: This module is still under construction, currently it only supports SpringBoot based Bots, we are going to
> provide the same support for java based Bots very soon.
## Java BDK Bots Integration tests

To create an integration test for a Java BDK Bot application, developer needs to add annotation `@SymphonyBdkTest` on top
of the test class. This annotation has three properties `botId`, `botName`, and `botDisplayName`, developer can choose
appropriate values per requirement, otherwise the default values are used.

```java
long botId() default 1L;

String botName() default "bdk-bot";

String botDisplayName() default "BDK Bot";
```

The annotation automatically brings the `Mockito` as well as `SymphonyBdkExtension` Junit extensions in the test, which will
initialise a `SymphonyBdk` mock instance, through `SymphonyBdk`, developer can get all BDK services, please remember all
services are simply **Mockito** mocked objects. Take the simple example below,

```java

@SymphonyBdkTest
public class SampleBdkIntegrationTest {
private final V4User initiator = new V4User().displayName("user").userId(2L);
private final V4Stream stream = new V4Stream().streamId("my-room");

private SymphonyBdk bdk;

@Test
@DisplayName("Reply upon received gif category form reply, inject bdk as property")
void gif_form_replyWithMessage() {

bdk.activities().register(new GifFormActivity(bdk.messages()));
// given
when(bdk.messages().send(anyString(), anyString())).thenReturn(mock(V4Message.class));

// when
Map<String, Object> values = new HashMap<>();
values.put("action", "submit");
values.put("category", "bdk");
pushEventToDataFeed(new V4Event().id("id").timestamp(Instant.now().toEpochMilli())
.initiator(new V4Initiator().user(initiator))
.payload(new V4Payload().symphonyElementsAction(
new V4SymphonyElementsAction().formId("gif-category-form")
.formMessageId("form-message-id")
.formValues(values)
.stream(stream)))
.type(SymphonyBdkTestUtils.V4EventType.SYMPHONYELEMENTSACTION.name()));

// then
verify(bdk.messages()).send(eq("my-room"), contains("Gif category is \"bdk\""));
}
}
```

in this example, the goal is to validate the `GifFormActivity`, it is expecting to have a message sent back to the room,
when a `gif-category-form` reply has received.

**Step 1**. As shown, a `SymphonyBdk` mock instance is inject as test class property.

**Step 2**. Register the being tested form activity through BDK activity service.

**Step 3**. Stub the injected mocked bdk messages service,

**Step 4**. Inject the form reply event through `SymphonyBdkTestUtils.java`,

**Step 5**. at the end we verify that a replied message has been sent back to the room through bdk messages service.


The `SymphonyBdk` instance can also be injected as test method argument, like shown in the example below.

```java

@SymphonyBdkTest
public class SampleBdkIntegrationTest {
private final V4User initiator = new V4User().displayName("user").userId(2L);
private final V4Stream stream = new V4Stream().streamId("my-room");

@Test
@DisplayName("Reply echo slash command, inject bdk as parameter")
void testEchoSlashCommand(SymphonyBdk bdk) {
final SlashCommand slashCommand = SlashCommand.slash("/echo {argument}",
false,
context -> bdk.messages()
.send(context.getStreamId(),
String.format(
"Received argument: %s",
context.getArguments()
.get("argument"))),
"echo slash command");
bdk.activities().register(slashCommand);

// given
when(bdk.messages().send(anyString(), any(Message.class))).thenReturn(mock(V4Message.class));

// when
pushMessageToDF(initiator, stream, "/echo arg");

// then
verify(bdk.messages()).send(eq("my-room"), contains("Received argument: arg"));
}
```

## SpringBoot based Bots Integration tests

To create an integration test, all developer needs is to add the annotation `@SymphonyBdkSpringBootTest` on top of the
test class, when the test is launched, a SpringBoot application context is going to be loaded, all BDK services are
being injected in this application context.
To create an integration test for an SpringBoot based Bot application, developer needs to add the
annotation `@SymphonyBdkSpringBootTest` on top of the test class, when the test is launched, a SpringBoot application
context is going to be loaded, all BDK services are being injected in this application context.

Developer can then `@Autowired` these BDK services beans whenever they are needed in the test, please note that these
services beans are simply **Mockito** mocked objects, they have to be stubbed just like the way how we do in a simple
Expand All @@ -19,7 +118,7 @@ JUnit test.
The annotation `@SymphonyBdkSpringBootTest` comes with a properties array attribute, which allows to define the Bot
information, such as `bot.id`, `bot.username`, and `bot.display-name`, the additional SpringBoot Bot application
properties can also be provided in this array (Another option is to use a YAML configuration file, please see the next
paragraph).
paragraph). By default, this array value is `"bot.id=1", "bot.username=bdk-bot", "bot.display-name=BDK Bot"`.

The test annotated with `@SymphonyBdkSpringBootTest` is automatically marked under SpringBoot `integration-test`
profile, so developer can
Expand All @@ -41,7 +140,11 @@ public class SampleSpringAppIntegrationTest {
private final V4Stream stream = new V4Stream().streamId("my-room");

@Test
void echo_command_replyWithMessage(@Autowired MessageService messageService, @Autowired UserV2 botInfo) {
void echo_command_replyWithMessage(
@Autowired
MessageService messageService,
@Autowired
UserV2 botInfo) {
// (1) given
when(messageService.send(anyString(), any(Message.class))).thenReturn(mock(V4Message.class));

Expand All @@ -68,7 +171,8 @@ message has received.
The `@SymphonyBdkSpringBootTest` annotation is inheritable. Developers may have one parent test class with this
annotation, so that the child test classes will inherit the annotation and its properties.

### Utils
## Symphony Bdk Test Utils

The `SymphonyBdkTestUtils.java` is a very handy helper class allowing to inject Symphony events to the DataFeed, so that
the registered activities and slash commands should react on these received events.

Expand All @@ -91,17 +195,17 @@ This util class comes with six methods so far,
```java

void test(){
...
...
pushEventToDataFeed(new V4Event()
...
...
pushEventToDataFeed(new V4Event()
.initiator(new V4Initiator().user(initiator))
.payload(new V4Payload().symphonyElementsAction(
new V4SymphonyElementsAction().formId("gif-category-form")
.formMessageId("form-message-id")
.formValues(values)
.stream(stream)))
new V4SymphonyElementsAction().formId("gif-category-form")
.formMessageId("form-message-id")
.formValues(values)
.stream(stream)))
.type(V4EventType.SYMPHONYELEMENTSACTION.name()));
}
}
```

----
Expand Down
2 changes: 1 addition & 1 deletion symphony-bdk-bom/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ repositories {

dependencies {
// import Spring Boot's BOM
api platform('org.springframework.boot:spring-boot-dependencies:2.7.9')
api platform('org.springframework.boot:spring-boot-dependencies:2.7.10')
// import Jackson's BOM
api platform('com.fasterxml.jackson:jackson-bom:2.14.1')
// define all our dependencies versions
Expand Down
2 changes: 2 additions & 0 deletions symphony-bdk-examples/bdk-core-examples/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ dependencies {
implementation 'commons-io:commons-io'
implementation 'org.apache.commons:commons-lang3'

testImplementation project(':symphony-bdk-test:symphony-bdk-test-jupiter')

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.symphony.bdk.examples;

import static com.symphony.bdk.test.SymphonyBdkTestUtils.pushEventToDataFeed;
import static com.symphony.bdk.test.SymphonyBdkTestUtils.pushMessageToDF;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.symphony.bdk.core.SymphonyBdk;
import com.symphony.bdk.core.activity.ActivityMatcher;
import com.symphony.bdk.core.activity.command.SlashCommand;
import com.symphony.bdk.core.activity.form.FormReplyActivity;
import com.symphony.bdk.core.activity.form.FormReplyContext;
import com.symphony.bdk.core.activity.model.ActivityInfo;
import com.symphony.bdk.core.activity.model.ActivityType;
import com.symphony.bdk.core.service.message.MessageService;
import com.symphony.bdk.core.service.message.model.Message;
import com.symphony.bdk.gen.api.model.V4Event;
import com.symphony.bdk.gen.api.model.V4Initiator;
import com.symphony.bdk.gen.api.model.V4Message;
import com.symphony.bdk.gen.api.model.V4Payload;
import com.symphony.bdk.gen.api.model.V4Stream;
import com.symphony.bdk.gen.api.model.V4SymphonyElementsAction;
import com.symphony.bdk.gen.api.model.V4User;
import com.symphony.bdk.test.SymphonyBdkTestUtils;
import com.symphony.bdk.test.annotation.SymphonyBdkTest;

import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

@SymphonyBdkTest
public class SampleBdkIntegrationTest {
private final V4User initiator = new V4User().displayName("user").userId(2L);
private final V4Stream stream = new V4Stream().streamId("my-room");

private SymphonyBdk bdk;

@Test
@DisplayName("Reply echo slash command, inject bdk as parameter")
void testEchoSlashCommand(SymphonyBdk bdk) {
final SlashCommand slashCommand = SlashCommand.slash("/echo {argument}", false,
context -> bdk.messages()
.send(context.getStreamId(),
String.format("Received argument: %s", context.getArguments().get("argument"))),
"echo slash command");
bdk.activities().register(slashCommand);

// given
when(bdk.messages().send(anyString(), any(Message.class))).thenReturn(mock(V4Message.class));

// when
pushMessageToDF(initiator, stream, "/echo arg");

// then
verify(bdk.messages()).send(eq("my-room"), contains("Received argument: arg"));
}

@Test
@DisplayName("Reply upon received gif category form reply, inject bdk as property")
void gif_form_replyWithMessage() {

bdk.activities().register(new GifFormActivity(bdk.messages()));

// given
when(bdk.messages().send(anyString(), anyString())).thenReturn(mock(V4Message.class));

// when
Map<String, Object> values = new HashMap<>();
values.put("action", "submit");
values.put("category", "bdk");
pushEventToDataFeed(new V4Event().id("id").timestamp(Instant.now().toEpochMilli())
.initiator(new V4Initiator().user(initiator))
.payload(new V4Payload().symphonyElementsAction(
new V4SymphonyElementsAction().formId("gif-category-form")
.formMessageId("form-message-id")
.formValues(values)
.stream(stream)))
.type(SymphonyBdkTestUtils.V4EventType.SYMPHONYELEMENTSACTION.name()));

// then
verify(bdk.messages()).send(eq("my-room"), contains("Gif category is \"bdk\""));
}

public static class GifFormActivity extends FormReplyActivity<FormReplyContext> {

private final MessageService messageService;

public GifFormActivity(MessageService messageService) {
this.messageService = messageService;
}

@Override
public ActivityMatcher<FormReplyContext> matcher() {
return context -> "gif-category-form".equals(context.getFormId())
&& "submit".equals(context.getFormValue("action"))
&& StringUtils.isNotEmpty(context.getFormValue("category"));
}

@Override
public void onActivity(FormReplyContext context) {
this.messageService.send(context.getStreamId(),
String.format("Gif category is \"%s\"", context.getFormValue("category")));
}

@Override
protected ActivityInfo info() {
return new ActivityInfo()
.type(ActivityType.FORM)
.name("Gif Display category form command")
.description("\"Form handler for the Gif Category form\"");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ public String apply(String message, UserV2 botInfo) {
messageMl.append(PREFIX_MENTION_TAG);
messageMl.append(botInfo.getDisplayName());
messageMl.append(SUFFIX_MENTION_TAG);
messageMl.append(" ");
}
messageMl.append(message);
messageMl.append(SUFFIX_MESSAGEML_TAG);
Expand Down

0 comments on commit e54e61b

Please sign in to comment.