2 changed files with 254 additions and 2 deletions
@ -0,0 +1,248 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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.rule.engine.action; |
|||
|
|||
import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; |
|||
import com.datastax.oss.driver.api.core.ProtocolVersion; |
|||
import com.datastax.oss.driver.api.core.cql.BoundStatement; |
|||
import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; |
|||
import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; |
|||
import com.datastax.oss.driver.api.core.cql.PreparedStatement; |
|||
import com.datastax.oss.driver.api.core.metadata.Node; |
|||
import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; |
|||
|
|||
import com.google.common.util.concurrent.SettableFuture; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.common.util.ListeningExecutor; |
|||
import org.thingsboard.rule.engine.TestDbCallbackExecutor; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.msg.TbMsgType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
|||
import org.thingsboard.server.dao.cassandra.guava.GuavaSession; |
|||
import org.thingsboard.server.dao.nosql.CassandraStatementTask; |
|||
import org.thingsboard.server.dao.nosql.TbResultSet; |
|||
import org.thingsboard.server.dao.nosql.TbResultSetFuture; |
|||
|
|||
import java.nio.ByteBuffer; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.UUID; |
|||
|
|||
import static java.util.Collections.emptyMap; |
|||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.anyInt; |
|||
import static org.mockito.ArgumentMatchers.anyLong; |
|||
import static org.mockito.ArgumentMatchers.anyString; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.doAnswer; |
|||
import static org.mockito.Mockito.spy; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class TbSaveToCustomCassandraTableNodeTest { |
|||
|
|||
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("ac4ca02e-2ae6-404a-8f7e-c4ae31c56aa7")); |
|||
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("64ad971e-9cfa-49e4-9f59-faa1a2350c6e")); |
|||
|
|||
private final ListeningExecutor dbCallbackExecutor = new TestDbCallbackExecutor(); |
|||
|
|||
private TbSaveToCustomCassandraTableNode node; |
|||
private TbSaveToCustomCassandraTableNodeConfiguration config; |
|||
|
|||
@Mock |
|||
private TbContext ctxMock; |
|||
@Mock |
|||
private CassandraCluster cassandraClusterMock; |
|||
@Mock |
|||
private GuavaSession sessionMock; |
|||
@Mock |
|||
private PreparedStatement preparedStatementMock; |
|||
@Mock |
|||
private BoundStatement boundStatementMock; |
|||
@Mock |
|||
private BoundStatementBuilder boundStatementBuilderMock; |
|||
@Mock |
|||
private ColumnDefinitions columnDefinitionsMock; |
|||
@Mock |
|||
private CodecRegistry codecRegistryMock; |
|||
@Mock |
|||
private ProtocolVersion protocolVersionMock; |
|||
@Mock |
|||
private Node nodeMock; |
|||
|
|||
@Test |
|||
void givenCassandraClusterIsMissing_whenInit_thenThrowsException() { |
|||
node = new TbSaveToCustomCassandraTableNode(); |
|||
config = new TbSaveToCustomCassandraTableNodeConfiguration().defaultConfiguration(); |
|||
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
assertThatThrownBy(() -> node.init(ctxMock, configuration)) |
|||
.isInstanceOf(RuntimeException.class) |
|||
.hasMessage("Unable to connect to Cassandra database"); |
|||
} |
|||
|
|||
@Test |
|||
void givenFieldsMapIsEmpty_whenInit_thenThrowsException() { |
|||
node = new TbSaveToCustomCassandraTableNode(); |
|||
config = new TbSaveToCustomCassandraTableNodeConfiguration().defaultConfiguration(); |
|||
config.setFieldsMapping(emptyMap()); |
|||
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
|
|||
when(ctxMock.getCassandraCluster()).thenReturn(cassandraClusterMock); |
|||
|
|||
assertThatThrownBy(() -> node.init(ctxMock, configuration)) |
|||
.isInstanceOf(RuntimeException.class) |
|||
.hasMessage("Fields(key,value) map is empty!"); |
|||
} |
|||
|
|||
@Test |
|||
void givenInvalidMessageStructure_whenOnMsg_thenThrowsException() throws TbNodeException { |
|||
node = new TbSaveToCustomCassandraTableNode(); |
|||
config = new TbSaveToCustomCassandraTableNodeConfiguration().defaultConfiguration(); |
|||
config.setTableName("temperature_sensor"); |
|||
config.setFieldsMapping(Map.of("temp", "temperature")); |
|||
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
|
|||
initializeMocks(); |
|||
|
|||
node.init(ctxMock, configuration); |
|||
|
|||
String data = ""; |
|||
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data); |
|||
|
|||
assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) |
|||
.isInstanceOf(IllegalStateException.class) |
|||
.hasMessage("Invalid message structure, it is not a JSON Object:" + null); |
|||
} |
|||
|
|||
@Test |
|||
void givenDataKeyIsMissingInMsg_whenOnMsg_thenThrowsException() throws TbNodeException { |
|||
node = new TbSaveToCustomCassandraTableNode(); |
|||
config = new TbSaveToCustomCassandraTableNodeConfiguration().defaultConfiguration(); |
|||
config.setTableName("temperature_sensor"); |
|||
config.setFieldsMapping(Map.of("temp", "temperature")); |
|||
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
|
|||
initializeMocks(); |
|||
when(preparedStatementMock.bind()).thenReturn(boundStatementMock); |
|||
when(boundStatementMock.getPreparedStatement()).thenReturn(preparedStatementMock); |
|||
when(preparedStatementMock.getVariableDefinitions()).thenReturn(columnDefinitionsMock); |
|||
when(boundStatementMock.codecRegistry()).thenReturn(codecRegistryMock); |
|||
when(boundStatementMock.protocolVersion()).thenReturn(protocolVersionMock); |
|||
when(boundStatementMock.getNode()).thenReturn(nodeMock); |
|||
|
|||
node.init(ctxMock, configuration); |
|||
|
|||
String data = """ |
|||
{ |
|||
"humidity": 77 |
|||
} |
|||
"""; |
|||
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data);; |
|||
|
|||
assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) |
|||
.isInstanceOf(RuntimeException.class) |
|||
.hasMessage("Message data doesn't contain key: 'temp'!"); |
|||
} |
|||
|
|||
@Test |
|||
void givenUnsupportedData_whenOnMsg_thenThrowsException() throws TbNodeException { |
|||
node = new TbSaveToCustomCassandraTableNode(); |
|||
config = new TbSaveToCustomCassandraTableNodeConfiguration().defaultConfiguration(); |
|||
config.setTableName("temperature_sensor"); |
|||
config.setFieldsMapping(Map.of("temp", "temperature")); |
|||
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
|
|||
initializeMocks(); |
|||
when(preparedStatementMock.bind()).thenReturn(boundStatementMock); |
|||
when(boundStatementMock.getPreparedStatement()).thenReturn(preparedStatementMock); |
|||
when(preparedStatementMock.getVariableDefinitions()).thenReturn(columnDefinitionsMock); |
|||
when(boundStatementMock.getValues()).thenReturn(List.of(ByteBuffer.allocate(1))); |
|||
when(boundStatementMock.codecRegistry()).thenReturn(codecRegistryMock); |
|||
when(boundStatementMock.protocolVersion()).thenReturn(protocolVersionMock); |
|||
when(boundStatementMock.getNode()).thenReturn(nodeMock); |
|||
|
|||
node.init(ctxMock, configuration); |
|||
|
|||
String data = """ |
|||
{ |
|||
"temp": [value] |
|||
} |
|||
"""; |
|||
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data); |
|||
|
|||
|
|||
assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) |
|||
.isInstanceOf(RuntimeException.class) |
|||
.hasMessage("Message data key: 'temp' with value: '[\"value\"]' is not a JSON Object or JSON Primitive!"); |
|||
} |
|||
|
|||
@Test |
|||
void givenValidMsgStructure_whenOnMsg_thenOk() throws Exception { |
|||
node = spy(new TbSaveToCustomCassandraTableNode()); |
|||
config = new TbSaveToCustomCassandraTableNodeConfiguration().defaultConfiguration(); |
|||
config.setTableName("temperature_sensor"); |
|||
config.setFieldsMapping(Map.of("temp", "temperature")); |
|||
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
|
|||
initializeMocks(); |
|||
doAnswer(invocation -> boundStatementBuilderMock).when(node).getStmtBuilder(); |
|||
when(boundStatementBuilderMock.setLong(anyInt(), anyLong())).thenReturn(boundStatementBuilderMock); |
|||
when(boundStatementBuilderMock.build()).thenReturn(boundStatementMock); |
|||
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
|||
doAnswer(invocation -> { |
|||
SettableFuture<TbResultSet> mainFuture = SettableFuture.create(); |
|||
mainFuture.set(new TbResultSet(null, null, null)); |
|||
TbResultSetFuture value = new TbResultSetFuture(mainFuture); |
|||
return value; |
|||
}).when(ctxMock).submitCassandraWriteTask(any()); |
|||
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbCallbackExecutor); |
|||
|
|||
node.init(ctxMock, configuration); |
|||
|
|||
String data = """ |
|||
{ |
|||
"temp": 31, |
|||
"humidity": 77 |
|||
} |
|||
"""; |
|||
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data); |
|||
|
|||
node.onMsg(ctxMock, msg); |
|||
|
|||
verify(sessionMock).prepare(eq("INSERT INTO cs_tb_temperature_sensor(temperature) VALUES(?)")); |
|||
verify(ctxMock).submitCassandraWriteTask(any(CassandraStatementTask.class)); |
|||
verify(ctxMock).tellSuccess(eq(msg)); |
|||
} |
|||
|
|||
private void initializeMocks() { |
|||
when(ctxMock.getCassandraCluster()).thenReturn(cassandraClusterMock); |
|||
when(cassandraClusterMock.getSession()).thenReturn(sessionMock); |
|||
when(cassandraClusterMock.getDefaultWriteConsistencyLevel()).thenReturn(DefaultConsistencyLevel.ONE); |
|||
when(sessionMock.prepare(anyString())).thenReturn(preparedStatementMock); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue