diff --git a/msa/integration-tests/README.md b/msa/integration-tests/README.md new file mode 100644 index 0000000000..5ae6354740 --- /dev/null +++ b/msa/integration-tests/README.md @@ -0,0 +1,18 @@ + +## Integration tests execution +To run the integration tests with using Docker, the local Docker images of Thingsboard's microservices should be built.
+- Build the local Docker images in the directory with the Thingsboard's main [pom.xml](./../../pom.xml): + + mvn clean install -Ddockerfile.skip=false +- Verify that the new local images were built: + + docker image ls +As result, in REPOSITORY column, next images should be present: + + local-maven-build/tb-node + local-maven-build/tb-web-ui + local-maven-build/tb-web-ui + +- Run the integration tests in the [msa/integration-tests](../integration-tests) directory: + + mvn clean install -Dintegrationtests.skip=false \ No newline at end of file diff --git a/msa/integration-tests/pom.xml b/msa/integration-tests/pom.xml new file mode 100644 index 0000000000..a1c24f39c6 --- /dev/null +++ b/msa/integration-tests/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + + org.thingsboard + 2.2.0-SNAPSHOT + msa + + org.thingsboard.msa + integration-tests + + ThingsBoard Integration Tests + https://thingsboard.io + Project for ThingsBoard integration tests with using Docker + + + UTF-8 + ${basedir}/../.. + true + 1.9.1 + 1.3.9 + + + + + org.testcontainers + testcontainers + ${testcontainers.version} + + + org.java-websocket + Java-WebSocket + ${java-websocket.version} + + + io.takari.junit + takari-cpsuite + + + ch.qos.logback + logback-classic + + + com.google.code.gson + gson + + + org.apache.commons + commons-lang3 + + + com.google.guava + guava + + + org.thingsboard + netty-mqtt + + + org.thingsboard + tools + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*TestSuite.java + + ${integrationtests.skip} + + + + + + diff --git a/msa/integration-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java new file mode 100644 index 0000000000..ab7f101f98 --- /dev/null +++ b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -0,0 +1,112 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.msa; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.*; +import org.thingsboard.client.tools.RestClient; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Slf4j +public abstract class AbstractContainerTest { + protected static String httpUrl; + protected static String wsUrl; + protected static RestClient restClient; + protected ObjectMapper mapper = new ObjectMapper(); + + @BeforeClass + public static void before() { + httpUrl = "http://localhost:" + ContainerTestSuite.composeContainer.getServicePort("tb-web-ui1", ContainerTestSuite.EXPOSED_PORT); + wsUrl = "ws://localhost:" + ContainerTestSuite.composeContainer.getServicePort("tb-web-ui1", ContainerTestSuite.EXPOSED_PORT); + restClient = new RestClient(httpUrl); + } + + protected Device createDevice(String name) { + return restClient.createDevice(name + RandomStringUtils.randomAlphanumeric(7), "DEFAULT"); + } + + protected WsClient subscribeToTelemetryWebSocket(DeviceId deviceId) throws URISyntaxException, InterruptedException { + WsClient mWs = new WsClient(new URI(wsUrl + "/api/ws/plugins/telemetry?token=" + restClient.getToken())); + mWs.connectBlocking(1, TimeUnit.SECONDS); + + JsonObject tsSubCmd = new JsonObject(); + tsSubCmd.addProperty("entityType", EntityType.DEVICE.name()); + tsSubCmd.addProperty("entityId", deviceId.toString()); + tsSubCmd.addProperty("scope", "LATEST_TELEMETRY"); + tsSubCmd.addProperty("cmdId", new Random().nextInt(100)); + tsSubCmd.addProperty("unsubscribe", false); + JsonArray wsTsSubCmds = new JsonArray(); + wsTsSubCmds.add(tsSubCmd); + JsonObject wsRequest = new JsonObject(); + wsRequest.add("tsSubCmds", wsTsSubCmds); + wsRequest.add("historyCmds", new JsonArray()); + wsRequest.add("attrSubCmds", new JsonArray()); + mWs.send(wsRequest.toString()); + return mWs; + } + + protected Map getExpectedLatestValues(long ts) { + return ImmutableMap.builder() + .put("booleanKey", ts) + .put("stringKey", ts) + .put("doubleKey", ts) + .put("longKey", ts) + .build(); + } + + protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, Long expectedTs, String expectedValue) { + List list = wsTelemetryResponse.getDataValuesByKey(key); + return expectedTs.equals(list.get(0)) && expectedValue.equals(list.get(1)); + } + + protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, String expectedValue) { + List list = wsTelemetryResponse.getDataValuesByKey(key); + return expectedValue.equals(list.get(1)); + } + + protected JsonObject createPayload(long ts) { + JsonObject values = createPayload(); + JsonObject payload = new JsonObject(); + payload.addProperty("ts", ts); + payload.add("values", values); + return payload; + } + + protected JsonObject createPayload() { + JsonObject values = new JsonObject(); + values.addProperty("stringKey", "value1"); + values.addProperty("booleanKey", true); + values.addProperty("doubleKey", 42.0); + values.addProperty("longKey", 73L); + + return values; + } + +} diff --git a/msa/integration-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java new file mode 100644 index 0000000000..fd2de229d0 --- /dev/null +++ b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.msa; + +import org.junit.ClassRule; +import org.junit.extensions.cpsuite.ClasspathSuite; +import org.junit.runner.RunWith; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.File; + +@RunWith(ClasspathSuite.class) +@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*"}) +public class ContainerTestSuite { + static final int EXPOSED_PORT = 8080; + + @ClassRule + public static DockerComposeContainer composeContainer = new DockerComposeContainer(new File("./../docker/docker-compose.yml")) + .withPull(false) + .withLocalCompose(true) + .withTailChildContainers(true) + .withExposedService("tb-web-ui1", EXPOSED_PORT, Wait.forHttp("/login")); +} diff --git a/msa/integration-tests/src/test/java/org/thingsboard/server/msa/WsClient.java b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/WsClient.java new file mode 100644 index 0000000000..2f05eee4ce --- /dev/null +++ b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/WsClient.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.msa; + +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; + +import java.net.URI; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +public class WsClient extends WebSocketClient { + private final BlockingQueue events; + private String message; + + public WsClient(URI serverUri) { + super(serverUri); + events = new ArrayBlockingQueue<>(100); + } + + @Override + public void onOpen(ServerHandshake serverHandshake) { + } + + @Override + public void onMessage(String message) { + events.add(message); + this.message = message; + } + + @Override + public void onClose(int code, String reason, boolean remote) { + events.clear(); + } + + @Override + public void onError(Exception ex) { + ex.printStackTrace(); + } + + public String getLastMessage() { + return this.message; + } +} \ No newline at end of file diff --git a/msa/integration-tests/src/test/java/org/thingsboard/server/msa/WsTelemetryResponse.java b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/WsTelemetryResponse.java new file mode 100644 index 0000000000..834c5d1449 --- /dev/null +++ b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/WsTelemetryResponse.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.msa; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Data +public class WsTelemetryResponse implements Serializable { + private int subscriptionId; + private int errorCode; + private String errorMsg; + private Map>> data; + private Map latestValues; + + public List getDataValuesByKey(String key) { + return data.entrySet().stream() + .filter(e -> e.getKey().equals(key)) + .flatMap(e -> e.getValue().stream().flatMap(Collection::stream)) + .collect(Collectors.toList()); + } +} diff --git a/msa/integration-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java new file mode 100644 index 0000000000..7cc0a0f6e3 --- /dev/null +++ b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.msa.connectivity; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.ResponseEntity; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.WsClient; +import org.thingsboard.server.msa.WsTelemetryResponse; + +import java.util.concurrent.TimeUnit; + +public class HttpClientTest extends AbstractContainerTest { + + @Test + public void telemetryUpdate() throws Exception { + restClient.login("tenant@thingsboard.org", "tenant"); + + Device device = createDevice("http_"); + DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + + WsClient mWs = subscribeToTelemetryWebSocket(device.getId()); + ResponseEntity deviceTelemetryResponse = restClient.getRestTemplate() + .postForEntity(httpUrl + "/api/v1/{credentialsId}/telemetry", + mapper.readTree(createPayload().toString()), + ResponseEntity.class, + deviceCredentials.getCredentialsId()); + Assert.assertTrue(deviceTelemetryResponse.getStatusCode().is2xxSuccessful()); + TimeUnit.SECONDS.sleep(1); + WsTelemetryResponse actualLatestTelemetry = mapper.readValue(mWs.getLastMessage(), WsTelemetryResponse.class); + + Assert.assertEquals(getExpectedLatestValues(123456789L).keySet(), actualLatestTelemetry.getLatestValues().keySet()); + + Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString())); + Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1")); + Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0))); + Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73))); + + restClient.getRestTemplate().delete(httpUrl + "/api/device/" + device.getId()); + } +} diff --git a/msa/integration-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java new file mode 100644 index 0000000000..4ad638ed15 --- /dev/null +++ b/msa/integration-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.msa.connectivity; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Data; +import org.junit.*; +import org.thingsboard.mqtt.MqttClient; +import org.thingsboard.mqtt.MqttClientConfig; +import org.thingsboard.mqtt.MqttHandler; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.WsClient; +import org.thingsboard.server.msa.WsTelemetryResponse; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.*; + +public class MqttClientTest extends AbstractContainerTest { + + @Test + public void telemetryUpload() throws Exception { + restClient.login("tenant@thingsboard.org", "tenant"); + Device device = createDevice("mqtt_"); + DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + + WsClient mWs = subscribeToTelemetryWebSocket(device.getId()); + MqttClient mqttClient = getMqttClient(deviceCredentials); + mqttClient.publish("v1/devices/me/telemetry", Unpooled.wrappedBuffer(createPayload().toString().getBytes())); + TimeUnit.SECONDS.sleep(1); + WsTelemetryResponse actualLatestTelemetry = mapper.readValue(mWs.getLastMessage(), WsTelemetryResponse.class); + + Assert.assertEquals(getExpectedLatestValues(123456789L).keySet(), actualLatestTelemetry.getLatestValues().keySet()); + + Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString())); + Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1")); + Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0))); + Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73))); + + restClient.getRestTemplate().delete(httpUrl + "/api/device/" + device.getId()); + } + + @Test + public void telemetryUploadWithTs() throws Exception { + long ts = 1451649600512L; + + restClient.login("tenant@thingsboard.org", "tenant"); + Device device = createDevice("mqtt_"); + DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + + WsClient mWs = subscribeToTelemetryWebSocket(device.getId()); + MqttClient mqttClient = getMqttClient(deviceCredentials); + mqttClient.publish("v1/devices/me/telemetry", Unpooled.wrappedBuffer(createPayload(ts).toString().getBytes())); + TimeUnit.SECONDS.sleep(1); + WsTelemetryResponse actualLatestTelemetry = mapper.readValue(mWs.getLastMessage(), WsTelemetryResponse.class); + + Assert.assertEquals(getExpectedLatestValues(ts), actualLatestTelemetry.getLatestValues()); + + Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", ts, Boolean.TRUE.toString())); + Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", ts, "value1")); + Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", ts, Double.toString(42.0))); + Assert.assertTrue(verify(actualLatestTelemetry, "longKey", ts, Long.toString(73))); + + restClient.getRestTemplate().delete(httpUrl + "/api/device/" + device.getId()); + } + + private MqttClient getMqttClient(DeviceCredentials deviceCredentials) throws InterruptedException { + MqttMessageListener queue = new MqttMessageListener(); + MqttClientConfig clientConfig = new MqttClientConfig(); + clientConfig.setClientId("MQTT client from test"); + clientConfig.setUsername(deviceCredentials.getCredentialsId()); + MqttClient mqttClient = MqttClient.create(clientConfig, queue); + mqttClient.connect("localhost", 1883).sync(); + return mqttClient; + } + + @Data + private class MqttMessageListener implements MqttHandler { + private final BlockingQueue events; + + private MqttMessageListener() { + events = new ArrayBlockingQueue<>(100); + } + + @Override + public void onMessage(String topic, ByteBuf message) { + events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8))); + } + } + + @Data + private class MqttEvent { + private final String topic; + private final String message; + } +} diff --git a/msa/pom.xml b/msa/pom.xml index 21ddb4dafe..dd4c3653ba 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -40,6 +40,7 @@ js-executor web-ui tb-node + integration-tests