committed by
GitHub
152 changed files with 7096 additions and 1768 deletions
@ -0,0 +1,40 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.service.component; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.rule.engine.api.RuleNode; |
||||
|
|
||||
|
@Data |
||||
|
public class RuleNodeClassInfo { |
||||
|
|
||||
|
private final Class<?> clazz; |
||||
|
private final RuleNode annotation; |
||||
|
private final boolean versioned; |
||||
|
|
||||
|
public String getClassName(){ |
||||
|
return clazz.getName(); |
||||
|
} |
||||
|
|
||||
|
public String getSimpleName() { |
||||
|
return clazz.getSimpleName(); |
||||
|
} |
||||
|
|
||||
|
public int getCurrentVersion() { |
||||
|
return annotation.version(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.api; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
|
||||
|
public interface TbVersionedNode extends TbNode { |
||||
|
|
||||
|
/** |
||||
|
* Upgrades the configuration from a specific version to the current version specified in the |
||||
|
* {@link RuleNode} annotation for the instance of {@link TbVersionedNode}. |
||||
|
* |
||||
|
* @param fromVersion The version from which the configuration needs to be upgraded. |
||||
|
* @param oldConfiguration The old configuration to be upgraded. |
||||
|
* @return A pair consisting of a Boolean flag indicating the success of the upgrade |
||||
|
* and a JsonNode representing the upgraded configuration. |
||||
|
* @throws TbNodeException If an error occurs during the upgrade process. |
||||
|
*/ |
||||
|
TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public abstract class TbAbstractFetchToNodeConfiguration { |
||||
|
|
||||
|
private FetchTo fetchTo; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
import static org.thingsboard.common.util.DonAsynchron.withCallback; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class TbAbstractGetEntityDataNode<T extends EntityId> extends TbAbstractGetMappedDataNode<T, TbGetEntityDataNodeConfiguration> { |
||||
|
|
||||
|
private final static String DATA_TO_FETCH_PROPERTY_NAME = "dataToFetch"; |
||||
|
private static final String OLD_DATA_TO_FETCH_PROPERTY_NAME = "telemetry"; |
||||
|
private final static String DATA_MAPPING_PROPERTY_NAME = "dataMapping"; |
||||
|
private static final String OLD_DATA_MAPPING_PROPERTY_NAME = "attrMapping"; |
||||
|
|
||||
|
private static final String DATA_TO_FETCH_VALIDATION_MSG = "DataToFetch property has invalid value: %s." + |
||||
|
" Only ATTRIBUTES and LATEST_TELEMETRY values supported!"; |
||||
|
|
||||
|
@Override |
||||
|
public void onMsg(TbContext ctx, TbMsg msg) { |
||||
|
var msgDataAsObjectNode = FetchTo.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null; |
||||
|
withCallback(findEntityAsync(ctx, msg.getOriginator()), |
||||
|
entityId -> processDataAndTell(ctx, msg, entityId, msgDataAsObjectNode), |
||||
|
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); |
||||
|
} |
||||
|
|
||||
|
protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator); |
||||
|
|
||||
|
protected void checkDataToFetchSupportedOrElseThrow(DataToFetch dataToFetch) throws TbNodeException { |
||||
|
if (dataToFetch == null || dataToFetch.equals(DataToFetch.FIELDS)) { |
||||
|
throw new TbNodeException(String.format(DATA_TO_FETCH_VALIDATION_MSG, dataToFetch)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected void processDataAndTell(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) { |
||||
|
DataToFetch dataToFetch = config.getDataToFetch(); |
||||
|
switch (dataToFetch) { |
||||
|
case ATTRIBUTES: |
||||
|
processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); |
||||
|
break; |
||||
|
case LATEST_TELEMETRY: |
||||
|
processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); |
||||
|
break; |
||||
|
case FIELDS: |
||||
|
processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected TbPair<Boolean, JsonNode> upgradeToUseFetchToAndDataToFetch(JsonNode oldConfiguration) throws TbNodeException { |
||||
|
var newConfigObjectNode = (ObjectNode) oldConfiguration; |
||||
|
if (!newConfigObjectNode.has(OLD_DATA_TO_FETCH_PROPERTY_NAME)) { |
||||
|
throw new TbNodeException("property to update: '" + OLD_DATA_TO_FETCH_PROPERTY_NAME + "' doesn't exists in configuration!"); |
||||
|
} |
||||
|
if (!newConfigObjectNode.has(OLD_DATA_MAPPING_PROPERTY_NAME)) { |
||||
|
throw new TbNodeException("property to update: '" + OLD_DATA_MAPPING_PROPERTY_NAME + "' doesn't exists in configuration!"); |
||||
|
} |
||||
|
newConfigObjectNode.set(DATA_MAPPING_PROPERTY_NAME, newConfigObjectNode.get(OLD_DATA_MAPPING_PROPERTY_NAME)); |
||||
|
newConfigObjectNode.remove(OLD_DATA_MAPPING_PROPERTY_NAME); |
||||
|
var value = newConfigObjectNode.get(OLD_DATA_TO_FETCH_PROPERTY_NAME).asText(); |
||||
|
if ("true".equals(value)) { |
||||
|
newConfigObjectNode.remove(OLD_DATA_TO_FETCH_PROPERTY_NAME); |
||||
|
newConfigObjectNode.put(DATA_TO_FETCH_PROPERTY_NAME, DataToFetch.LATEST_TELEMETRY.name()); |
||||
|
} else if ("false".equals(value)) { |
||||
|
newConfigObjectNode.remove(OLD_DATA_TO_FETCH_PROPERTY_NAME); |
||||
|
newConfigObjectNode.put(DATA_TO_FETCH_PROPERTY_NAME, DataToFetch.ATTRIBUTES.name()); |
||||
|
} else { |
||||
|
throw new TbNodeException("property to update: '" + OLD_DATA_TO_FETCH_PROPERTY_NAME + "' has unexpected value: " + value + ". Allowed values: true or false!"); |
||||
|
} |
||||
|
newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, FetchTo.METADATA.name()); |
||||
|
return new TbPair<>(true, newConfigObjectNode); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,153 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import com.google.common.util.concurrent.MoreExecutors; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
||||
|
import org.thingsboard.rule.engine.util.EntitiesFieldsAsyncLoader; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.kv.KvEntry; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
import static org.thingsboard.common.util.DonAsynchron.withCallback; |
||||
|
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class TbAbstractGetMappedDataNode<T extends EntityId, C extends TbGetMappedDataNodeConfiguration> extends TbAbstractNodeWithFetchTo<C> { |
||||
|
|
||||
|
protected void checkIfMappingIsNotEmptyOrElseThrow(Map<String, String> dataMapping) throws TbNodeException { |
||||
|
if (dataMapping == null || dataMapping.isEmpty()) { |
||||
|
throw new TbNodeException("At least one mapping entry should be specified!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected void processFieldsData(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode, boolean ignoreNullStrings) { |
||||
|
var mappingsMap = processFieldsMappingPatterns(msg); |
||||
|
withCallback(getEntityFieldsAsync(ctx, entityId, mappingsMap, ignoreNullStrings), |
||||
|
data -> putFieldsDataAndTell(ctx, msg, msgDataAsJsonNode, data), |
||||
|
t -> ctx.tellFailure(msg, t), |
||||
|
MoreExecutors.directExecutor()); |
||||
|
} |
||||
|
|
||||
|
protected void processAttributesKvEntryData(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) { |
||||
|
var mappingsMap = processKvEntryMappingPatterns(msg); |
||||
|
var sourceKeys = List.copyOf(mappingsMap.keySet()); |
||||
|
withCallback(getAttributesAsync(ctx, entityId, sourceKeys), |
||||
|
data -> putKvEntryDataAndTell(ctx, msg, data, mappingsMap, msgDataAsJsonNode), |
||||
|
t -> ctx.tellFailure(msg, t), |
||||
|
MoreExecutors.directExecutor()); |
||||
|
} |
||||
|
|
||||
|
protected void processTsKvEntryData(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) { |
||||
|
var mappingsMap = processKvEntryMappingPatterns(msg); |
||||
|
var sourceKeys = List.copyOf(mappingsMap.keySet()); |
||||
|
withCallback(getLatestTelemetryAsync(ctx, entityId, sourceKeys), |
||||
|
data -> putKvEntryDataAndTell(ctx, msg, data, mappingsMap, msgDataAsJsonNode), |
||||
|
t -> ctx.tellFailure(msg, t), |
||||
|
MoreExecutors.directExecutor()); |
||||
|
} |
||||
|
|
||||
|
private void putFieldsDataAndTell(TbContext ctx, TbMsg msg, ObjectNode msgDataAsJsonNode, Map<String, String> targetKeysToSourceValuesMap) { |
||||
|
TbMsgMetaData msgMetaData = msg.getMetaData().copy(); |
||||
|
for (var entry : targetKeysToSourceValuesMap.entrySet()) { |
||||
|
var targetKeyName = entry.getKey(); |
||||
|
var sourceFieldValue = entry.getValue(); |
||||
|
if (FetchTo.DATA.equals(fetchTo)) { |
||||
|
msgDataAsJsonNode.put(targetKeyName, sourceFieldValue); |
||||
|
} else if (FetchTo.METADATA.equals(fetchTo)) { |
||||
|
msgMetaData.putValue(targetKeyName, sourceFieldValue); |
||||
|
} |
||||
|
} |
||||
|
TbMsg outMsg = transformMessage(msg, msgDataAsJsonNode, msgMetaData); |
||||
|
ctx.tellSuccess(outMsg); |
||||
|
} |
||||
|
|
||||
|
private void putKvEntryDataAndTell(TbContext ctx, TbMsg msg, List<? extends KvEntry> data, Map<String, String> map, ObjectNode msgData) { |
||||
|
var msgMetaData = msg.getMetaData().copy(); |
||||
|
for (KvEntry entry : data) { |
||||
|
String targetKey = map.get(entry.getKey()); |
||||
|
enrichMessage(msgData, msgMetaData, entry, targetKey); |
||||
|
} |
||||
|
ctx.tellSuccess(transformMessage(msg, msgData, msgMetaData)); |
||||
|
} |
||||
|
|
||||
|
private Map<String, String> processFieldsMappingPatterns(TbMsg msg) { |
||||
|
var mappingsMap = new HashMap<String, String>(); |
||||
|
config.getDataMapping().forEach((sourceField, targetKey) -> { |
||||
|
String patternProcessedTargetKey = TbNodeUtils.processPattern(targetKey, msg); |
||||
|
mappingsMap.put(sourceField, patternProcessedTargetKey); |
||||
|
}); |
||||
|
return mappingsMap; |
||||
|
} |
||||
|
|
||||
|
private Map<String, String> processKvEntryMappingPatterns(TbMsg msg) { |
||||
|
var mappingsMap = new HashMap<String, String>(); |
||||
|
config.getDataMapping().forEach((sourceKey, targetKey) -> { |
||||
|
String patternProcessedSourceKey = TbNodeUtils.processPattern(sourceKey, msg); |
||||
|
String patternProcessedTargetKey = TbNodeUtils.processPattern(targetKey, msg); |
||||
|
mappingsMap.put(patternProcessedSourceKey, patternProcessedTargetKey); |
||||
|
}); |
||||
|
return mappingsMap; |
||||
|
} |
||||
|
|
||||
|
private ListenableFuture<Map<String, String>> getEntityFieldsAsync(TbContext ctx, EntityId entityId, Map<String, String> mappingsMap, boolean ignoreNullStrings) { |
||||
|
return Futures.transform(EntitiesFieldsAsyncLoader.findAsync(ctx, entityId), |
||||
|
fieldsData -> { |
||||
|
var targetKeysToSourceValuesMap = new HashMap<String, String>(); |
||||
|
for (var mappingEntry : mappingsMap.entrySet()) { |
||||
|
var sourceFieldName = mappingEntry.getKey(); |
||||
|
var targetKeyName = mappingEntry.getValue(); |
||||
|
var sourceFieldValue = fieldsData.getFieldValue(sourceFieldName, ignoreNullStrings); |
||||
|
if (sourceFieldValue != null) { |
||||
|
targetKeysToSourceValuesMap.put(targetKeyName, sourceFieldValue); |
||||
|
} |
||||
|
} |
||||
|
return targetKeysToSourceValuesMap; |
||||
|
}, ctx.getDbCallbackExecutor() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private ListenableFuture<List<KvEntry>> getAttributesAsync(TbContext ctx, EntityId entityId, List<String> attrKeys) { |
||||
|
var latest = ctx.getAttributesService().find(ctx.getTenantId(), entityId, SERVER_SCOPE, attrKeys); |
||||
|
return Futures.transform(latest, l -> |
||||
|
l.stream() |
||||
|
.map(i -> (KvEntry) i) |
||||
|
.collect(Collectors.toList()), |
||||
|
ctx.getDbCallbackExecutor()); |
||||
|
} |
||||
|
|
||||
|
private ListenableFuture<List<KvEntry>> getLatestTelemetryAsync(TbContext ctx, EntityId entityId, List<String> timeseriesKeys) { |
||||
|
var latest = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), entityId, timeseriesKeys); |
||||
|
return Futures.transform(latest, l -> |
||||
|
l.stream() |
||||
|
.map(i -> (KvEntry) i) |
||||
|
.collect(Collectors.toList()), |
||||
|
ctx.getDbCallbackExecutor()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import com.google.common.util.concurrent.AsyncFunction; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.TbVersionedNode; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.kv.KvEntry; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
|
||||
|
import java.util.NoSuchElementException; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class TbAbstractNodeWithFetchTo<C extends TbAbstractFetchToNodeConfiguration> implements TbVersionedNode { |
||||
|
|
||||
|
protected final static String FETCH_TO_PROPERTY_NAME = "fetchTo"; |
||||
|
|
||||
|
protected C config; |
||||
|
protected FetchTo fetchTo; |
||||
|
|
||||
|
@Override |
||||
|
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
||||
|
config = loadNodeConfiguration(configuration); |
||||
|
if (config.getFetchTo() == null) { |
||||
|
throw new TbNodeException("FetchTo cannot be null!"); |
||||
|
} else { |
||||
|
fetchTo = config.getFetchTo(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract C loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException; |
||||
|
|
||||
|
protected <I extends EntityId> AsyncFunction<I, I> checkIfEntityIsPresentOrThrow(String message) { |
||||
|
return id -> { |
||||
|
if (id == null || id.isNullUid()) { |
||||
|
return Futures.immediateFailedFuture(new NoSuchElementException(message)); |
||||
|
} |
||||
|
return Futures.immediateFuture(id); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
protected ObjectNode getMsgDataAsObjectNode(TbMsg msg) { |
||||
|
var msgDataNode = JacksonUtil.toJsonNode(msg.getData()); |
||||
|
if (msgDataNode == null || !msgDataNode.isObject()) { |
||||
|
throw new IllegalArgumentException("Message body is not an object!"); |
||||
|
} |
||||
|
return (ObjectNode) msgDataNode; |
||||
|
} |
||||
|
|
||||
|
protected void enrichMessage(ObjectNode msgData, TbMsgMetaData metaData, KvEntry kvEntry, String targetKey) { |
||||
|
if (FetchTo.DATA.equals(fetchTo)) { |
||||
|
JacksonUtil.addKvEntry(msgData, kvEntry, targetKey); |
||||
|
} else if (FetchTo.METADATA.equals(fetchTo)) { |
||||
|
metaData.putValue(targetKey, kvEntry.getValueAsString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected TbMsg transformMessage(TbMsg msg, ObjectNode msgDataNode, TbMsgMetaData msgMetaData) { |
||||
|
switch (fetchTo) { |
||||
|
case DATA: |
||||
|
return TbMsg.transformMsgData(msg, JacksonUtil.toString(msgDataNode)); |
||||
|
case METADATA: |
||||
|
return TbMsg.transformMsg(msg, msgMetaData); |
||||
|
default: |
||||
|
log.debug("Unexpected FetchTo value: {}. Allowed values: {}", fetchTo, FetchTo.values()); |
||||
|
return msg; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected TbPair<Boolean, JsonNode> upgradeRuleNodesWithOldPropertyToUseFetchTo( |
||||
|
JsonNode oldConfiguration, |
||||
|
String oldProperty, |
||||
|
String ifTrue, |
||||
|
String ifFalse |
||||
|
) throws TbNodeException { |
||||
|
var newConfigObjectNode = (ObjectNode) oldConfiguration; |
||||
|
if (!newConfigObjectNode.has(oldProperty)) { |
||||
|
throw new TbNodeException("property to update: '" + oldProperty + "' doesn't exists in configuration!"); |
||||
|
} |
||||
|
var value = newConfigObjectNode.get(oldProperty).asText(); |
||||
|
if ("true".equals(value)) { |
||||
|
newConfigObjectNode.remove(oldProperty); |
||||
|
newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, ifTrue); |
||||
|
return new TbPair<>(true, newConfigObjectNode); |
||||
|
} else if ("false".equals(value)) { |
||||
|
newConfigObjectNode.remove(oldProperty); |
||||
|
newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, ifFalse); |
||||
|
return new TbPair<>(true, newConfigObjectNode); |
||||
|
} else { |
||||
|
throw new TbNodeException("property to update: '" + oldProperty + "' has unexpected value: " + value + ". Allowed values: true or false!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,109 +0,0 @@ |
|||||
/** |
|
||||
* Copyright © 2016-2023 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.metadata; |
|
||||
|
|
||||
import com.google.common.util.concurrent.Futures; |
|
||||
import com.google.common.util.concurrent.ListenableFuture; |
|
||||
import com.google.common.util.concurrent.MoreExecutors; |
|
||||
import lombok.extern.slf4j.Slf4j; |
|
||||
import org.thingsboard.rule.engine.api.TbContext; |
|
||||
import org.thingsboard.rule.engine.api.TbNode; |
|
||||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|
||||
import org.thingsboard.rule.engine.api.TbNodeException; |
|
||||
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
|
||||
import org.thingsboard.server.common.data.id.EntityId; |
|
||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
|
||||
import org.thingsboard.server.common.data.kv.KvEntry; |
|
||||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|
||||
import org.thingsboard.server.common.msg.TbMsg; |
|
||||
|
|
||||
import java.util.HashMap; |
|
||||
import java.util.List; |
|
||||
import java.util.Map; |
|
||||
import java.util.stream.Collectors; |
|
||||
|
|
||||
import static org.thingsboard.common.util.DonAsynchron.withCallback; |
|
||||
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; |
|
||||
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; |
|
||||
|
|
||||
@Slf4j |
|
||||
public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode { |
|
||||
|
|
||||
private TbGetEntityAttrNodeConfiguration config; |
|
||||
|
|
||||
@Override |
|
||||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
|
||||
this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onMsg(TbContext ctx, TbMsg msg) { |
|
||||
try { |
|
||||
withCallback(findEntityAsync(ctx, msg.getOriginator()), |
|
||||
entityId -> safeGetAttributes(ctx, msg, entityId), |
|
||||
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); |
|
||||
} catch (Throwable th) { |
|
||||
ctx.tellFailure(msg, th); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void safeGetAttributes(TbContext ctx, TbMsg msg, T entityId) { |
|
||||
if (entityId == null || entityId.isNullUid()) { |
|
||||
ctx.tellNext(msg, FAILURE); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
Map<String, String> mappingsMap = new HashMap<>(); |
|
||||
config.getAttrMapping().forEach((key, value) -> { |
|
||||
String processPatternKey = TbNodeUtils.processPattern(key, msg); |
|
||||
String processPatternValue = TbNodeUtils.processPattern(value, msg); |
|
||||
mappingsMap.put(processPatternKey, processPatternValue); |
|
||||
}); |
|
||||
|
|
||||
List<String> keys = List.copyOf(mappingsMap.keySet()); |
|
||||
withCallback(config.isTelemetry() ? getLatestTelemetry(ctx, entityId, keys) : getAttributesAsync(ctx, entityId, keys), |
|
||||
attributes -> putAttributesAndTell(ctx, msg, attributes, mappingsMap), |
|
||||
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); |
|
||||
} |
|
||||
|
|
||||
private ListenableFuture<List<KvEntry>> getAttributesAsync(TbContext ctx, EntityId entityId, List<String> attrKeys) { |
|
||||
ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(ctx.getTenantId(), entityId, SERVER_SCOPE, attrKeys); |
|
||||
return Futures.transform(latest, l -> |
|
||||
l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()), MoreExecutors.directExecutor()); |
|
||||
} |
|
||||
|
|
||||
private ListenableFuture<List<KvEntry>> getLatestTelemetry(TbContext ctx, EntityId entityId, List<String> timeseriesKeys) { |
|
||||
ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), entityId, timeseriesKeys); |
|
||||
return Futures.transform(latest, l -> |
|
||||
l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()), MoreExecutors.directExecutor()); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
private void putAttributesAndTell(TbContext ctx, TbMsg msg, List<? extends KvEntry> attributes, Map<String, String> map) { |
|
||||
attributes.forEach(r -> { |
|
||||
String attrName = map.get(r.getKey()); |
|
||||
msg.getMetaData().putValue(attrName, r.getValueAsString()); |
|
||||
}); |
|
||||
ctx.tellSuccess(msg); |
|
||||
} |
|
||||
|
|
||||
protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator); |
|
||||
|
|
||||
public void setConfig(TbGetEntityAttrNodeConfiguration config) { |
|
||||
this.config = config; |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public abstract class TbGetMappedDataNodeConfiguration extends TbAbstractFetchToNodeConfiguration { |
||||
|
|
||||
|
private Map<String, String> dataMapping; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.util; |
||||
|
|
||||
|
import lombok.Getter; |
||||
|
|
||||
|
public enum ContactBasedEntityDetails { |
||||
|
|
||||
|
ID("id"), |
||||
|
TITLE("title"), |
||||
|
COUNTRY("country"), |
||||
|
CITY("city"), |
||||
|
STATE("state"), |
||||
|
ZIP("zip"), |
||||
|
ADDRESS("address"), |
||||
|
ADDRESS2("address2"), |
||||
|
PHONE("phone"), |
||||
|
EMAIL("email"), |
||||
|
ADDITIONAL_INFO("additionalInfo"); |
||||
|
|
||||
|
@Getter |
||||
|
private final String ruleEngineName; |
||||
|
|
||||
|
ContactBasedEntityDetails(String ruleEngineName) { |
||||
|
this.ruleEngineName = ruleEngineName; |
||||
|
} |
||||
|
|
||||
|
} |
||||
File diff suppressed because one or more lines are too long
@ -1,231 +0,0 @@ |
|||||
/** |
|
||||
* Copyright © 2016-2023 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.metadata; |
|
||||
|
|
||||
import com.datastax.oss.driver.api.core.uuid.Uuids; |
|
||||
import com.google.common.collect.Lists; |
|
||||
import com.google.common.util.concurrent.Futures; |
|
||||
import org.junit.runner.RunWith; |
|
||||
import org.mockito.ArgumentCaptor; |
|
||||
import org.mockito.Mock; |
|
||||
import org.mockito.junit.MockitoJUnitRunner; |
|
||||
import org.thingsboard.common.util.JacksonUtil; |
|
||||
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.Device; |
|
||||
import org.thingsboard.server.common.data.User; |
|
||||
import org.thingsboard.server.common.data.asset.Asset; |
|
||||
import org.thingsboard.server.common.data.id.CustomerId; |
|
||||
import org.thingsboard.server.common.data.id.EntityId; |
|
||||
import org.thingsboard.server.common.data.id.RuleChainId; |
|
||||
import org.thingsboard.server.common.data.id.RuleNodeId; |
|
||||
import org.thingsboard.server.common.data.id.TenantId; |
|
||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
|
||||
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; |
|
||||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|
||||
import org.thingsboard.server.common.data.kv.StringDataEntry; |
|
||||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|
||||
import org.thingsboard.server.common.msg.TbMsg; |
|
||||
import org.thingsboard.server.common.msg.TbMsgDataType; |
|
||||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|
||||
import org.thingsboard.server.dao.asset.AssetService; |
|
||||
import org.thingsboard.server.dao.attributes.AttributesService; |
|
||||
import org.thingsboard.server.dao.device.DeviceService; |
|
||||
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
|
||||
import org.thingsboard.server.dao.user.UserService; |
|
||||
|
|
||||
import java.util.HashMap; |
|
||||
import java.util.List; |
|
||||
import java.util.Map; |
|
||||
|
|
||||
import static org.junit.Assert.assertEquals; |
|
||||
import static org.junit.Assert.assertTrue; |
|
||||
import static org.mockito.ArgumentMatchers.any; |
|
||||
import static org.mockito.ArgumentMatchers.anyCollection; |
|
||||
import static org.mockito.ArgumentMatchers.eq; |
|
||||
import static org.mockito.ArgumentMatchers.same; |
|
||||
import static org.mockito.Mockito.verify; |
|
||||
import static org.mockito.Mockito.when; |
|
||||
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; |
|
||||
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; |
|
||||
|
|
||||
@RunWith(MockitoJUnitRunner.class) |
|
||||
public abstract class AbstractAttributeNodeTest { |
|
||||
final CustomerId customerId = new CustomerId(Uuids.timeBased()); |
|
||||
final TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); |
|
||||
final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); |
|
||||
final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); |
|
||||
final String keyAttrConf = "${word}"; |
|
||||
final String valueAttrConf = "${result}"; |
|
||||
@Mock |
|
||||
TbContext ctx; |
|
||||
@Mock |
|
||||
AttributesService attributesService; |
|
||||
@Mock |
|
||||
TimeseriesService timeseriesService; |
|
||||
@Mock |
|
||||
UserService userService; |
|
||||
@Mock |
|
||||
AssetService assetService; |
|
||||
@Mock |
|
||||
DeviceService deviceService; |
|
||||
TbMsg msg; |
|
||||
Map<String, String> metaData; |
|
||||
TbEntityGetAttrNode node; |
|
||||
|
|
||||
void init(TbEntityGetAttrNode node) throws TbNodeException { |
|
||||
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(getTbNodeConfig())); |
|
||||
|
|
||||
metaData = new HashMap<>(); |
|
||||
metaData.putIfAbsent("word", "temperature"); |
|
||||
metaData.putIfAbsent("result", "answer"); |
|
||||
|
|
||||
this.node = node; |
|
||||
this.node.init(null, nodeConfiguration); |
|
||||
} |
|
||||
|
|
||||
void errorThrownIfCannotLoadAttributes(User user) { |
|
||||
msg = TbMsg.newMsg("USER", user.getId(), new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); |
|
||||
|
|
||||
when(ctx.getAttributesService()).thenReturn(attributesService); |
|
||||
when(attributesService.find(any(), eq(getEntityId()), eq(SERVER_SCOPE), anyCollection())) |
|
||||
.thenThrow(new IllegalStateException("something wrong")); |
|
||||
|
|
||||
node.onMsg(ctx, msg); |
|
||||
final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class); |
|
||||
verify(ctx).tellFailure(same(msg), captor.capture()); |
|
||||
|
|
||||
Throwable value = captor.getValue(); |
|
||||
assertEquals("something wrong", value.getMessage()); |
|
||||
assertTrue(msg.getMetaData().getData().isEmpty()); |
|
||||
} |
|
||||
|
|
||||
void errorThrownIfCannotLoadAttributesAsync(User user) { |
|
||||
|
|
||||
msg = TbMsg.newMsg("USER", user.getId(), new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); |
|
||||
|
|
||||
when(ctx.getAttributesService()).thenReturn(attributesService); |
|
||||
when(attributesService.find(any(), eq(getEntityId()), eq(SERVER_SCOPE), anyCollection())) |
|
||||
.thenReturn(Futures.immediateFailedFuture(new IllegalStateException("something wrong"))); |
|
||||
|
|
||||
node.onMsg(ctx, msg); |
|
||||
final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class); |
|
||||
verify(ctx).tellFailure(same(msg), captor.capture()); |
|
||||
|
|
||||
Throwable value = captor.getValue(); |
|
||||
assertEquals("something wrong", value.getMessage()); |
|
||||
assertTrue(msg.getMetaData().getData().isEmpty()); |
|
||||
} |
|
||||
|
|
||||
void failedChainUsedIfCustomerCannotBeFound(User user) { |
|
||||
msg = TbMsg.newMsg("USER", user.getId(), new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); |
|
||||
|
|
||||
node.onMsg(ctx, msg); |
|
||||
verify(ctx).tellNext(msg, FAILURE); |
|
||||
assertTrue(msg.getMetaData().getData().isEmpty()); |
|
||||
} |
|
||||
|
|
||||
void entityAttributeAddedInMetadata(EntityId entityId, String type) { |
|
||||
msg = TbMsg.newMsg(type, entityId, new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); |
|
||||
entityAttributeFetched(getEntityId()); |
|
||||
} |
|
||||
|
|
||||
void usersCustomerAttributesFetched(User user) { |
|
||||
msg = TbMsg.newMsg("USER", user.getId(), new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); |
|
||||
|
|
||||
entityAttributeFetched(getEntityId()); |
|
||||
} |
|
||||
|
|
||||
void assetsCustomerAttributesFetched(Asset asset) { |
|
||||
msg = TbMsg.newMsg("ASSET", asset.getId(), new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); |
|
||||
|
|
||||
entityAttributeFetched(getEntityId()); |
|
||||
} |
|
||||
|
|
||||
void deviceCustomerAttributesFetched(Device device) { |
|
||||
msg = TbMsg.newMsg("DEVICE", device.getId(), new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); |
|
||||
|
|
||||
entityAttributeFetched(getEntityId()); |
|
||||
} |
|
||||
|
|
||||
void deviceCustomerTelemetryFetched(Device device) throws TbNodeException { |
|
||||
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(getTbNodeConfigForTelemetry())); |
|
||||
|
|
||||
TbEntityGetAttrNode node = getEmptyNode(); |
|
||||
node.init(null, nodeConfiguration); |
|
||||
|
|
||||
msg = TbMsg.newMsg("DEVICE", device.getId(), new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); |
|
||||
|
|
||||
List<TsKvEntry> timeseries = Lists.newArrayList(new BasicTsKvEntry(1L, new StringDataEntry("temperature", "highest"))); |
|
||||
|
|
||||
when(ctx.getTimeseriesService()).thenReturn(timeseriesService); |
|
||||
when(timeseriesService.findLatest(any(), eq(getEntityId()), anyCollection())) |
|
||||
.thenReturn(Futures.immediateFuture(timeseries)); |
|
||||
|
|
||||
node.onMsg(ctx, msg); |
|
||||
verify(ctx).tellSuccess(msg); |
|
||||
assertEquals(msg.getMetaData().getValue("answer"), "highest"); |
|
||||
} |
|
||||
|
|
||||
void entityAttributeFetched(EntityId entityId) { |
|
||||
List<AttributeKvEntry> attributes = Lists.newArrayList(new BaseAttributeKvEntry(new StringDataEntry("temperature", "high"), 1L)); |
|
||||
|
|
||||
when(ctx.getAttributesService()).thenReturn(attributesService); |
|
||||
when(attributesService.find(any(), eq(entityId), eq(SERVER_SCOPE), anyCollection())) |
|
||||
.thenReturn(Futures.immediateFuture(attributes)); |
|
||||
|
|
||||
node.onMsg(ctx, msg); |
|
||||
verify(ctx).tellSuccess(msg); |
|
||||
assertEquals(msg.getMetaData().getValue("answer"), "high"); |
|
||||
} |
|
||||
|
|
||||
TbGetEntityAttrNodeConfiguration getTbNodeConfig() { |
|
||||
return getConfig(false); |
|
||||
} |
|
||||
|
|
||||
TbGetEntityAttrNodeConfiguration getTbNodeConfigForTelemetry() { |
|
||||
return getConfig(true); |
|
||||
} |
|
||||
|
|
||||
private TbGetEntityAttrNodeConfiguration getConfig(boolean isTelemetry) { |
|
||||
TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration(); |
|
||||
Map<String, String> conf = new HashMap<>(); |
|
||||
conf.put(keyAttrConf, valueAttrConf); |
|
||||
config.setAttrMapping(conf); |
|
||||
config.setTelemetry(isTelemetry); |
|
||||
return config; |
|
||||
} |
|
||||
|
|
||||
protected abstract TbEntityGetAttrNode getEmptyNode(); |
|
||||
|
|
||||
abstract EntityId getEntityId(); |
|
||||
|
|
||||
void mockFindDevice(Device device) { |
|
||||
when(ctx.getDeviceService()).thenReturn(deviceService); |
|
||||
when(deviceService.findDeviceById(any(), eq(device.getId()))).thenReturn(device); |
|
||||
} |
|
||||
|
|
||||
void mockFindAsset(Asset asset) { |
|
||||
when(ctx.getAssetService()).thenReturn(assetService); |
|
||||
when(assetService.findAssetByIdAsync(any(), eq(asset.getId()))).thenReturn(Futures.immediateFuture(asset)); |
|
||||
} |
|
||||
|
|
||||
void mockFindUser(User user) { |
|
||||
when(ctx.getUserService()).thenReturn(userService); |
|
||||
when(userService.findUserByIdAsync(any(), eq(user.getId()))).thenReturn(Futures.immediateFuture(user)); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,449 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.assertj.core.api.Assertions; |
||||
|
import org.jetbrains.annotations.NotNull; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
import org.mockito.ArgumentMatcher; |
||||
|
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.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.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BooleanDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.DoubleDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.JsonDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.LongDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.Callable; |
||||
|
|
||||
|
import static org.junit.jupiter.api.Assertions.assertEquals; |
||||
|
import static org.junit.jupiter.api.Assertions.assertFalse; |
||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf; |
||||
|
import static org.junit.jupiter.api.Assertions.assertTrue; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.anyList; |
||||
|
import static org.mockito.ArgumentMatchers.anySet; |
||||
|
import static org.mockito.ArgumentMatchers.anyString; |
||||
|
import static org.mockito.ArgumentMatchers.argThat; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.Mockito.never; |
||||
|
import static org.mockito.Mockito.reset; |
||||
|
import static org.mockito.Mockito.times; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class CalculateDeltaNodeTest { |
||||
|
|
||||
|
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); |
||||
|
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); |
||||
|
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { |
||||
|
@Override |
||||
|
public <T> ListenableFuture<T> executeAsync(Callable<T> task) { |
||||
|
try { |
||||
|
return Futures.immediateFuture(task.call()); |
||||
|
} catch (Exception e) { |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute(@NotNull Runnable command) { |
||||
|
command.run(); |
||||
|
} |
||||
|
}; |
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private TimeseriesService timeseriesServiceMock; |
||||
|
private CalculateDeltaNode node; |
||||
|
private CalculateDeltaNodeConfiguration config; |
||||
|
private TbNodeConfiguration nodeConfiguration; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() throws TbNodeException { |
||||
|
node = new CalculateDeltaNode(); |
||||
|
config = new CalculateDeltaNodeConfiguration().defaultConfiguration(); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock); |
||||
|
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenDefaultConfig_whenDefaultConfiguration_thenVerify() { |
||||
|
assertEquals(config.getInputValueKey(), "pulseCounter"); |
||||
|
assertEquals(config.getOutputValueKey(), "delta"); |
||||
|
assertTrue(config.isUseCache()); |
||||
|
assertFalse(config.isAddPeriodBetweenMsgs()); |
||||
|
assertEquals(config.getPeriodValueKey(), "periodInMs"); |
||||
|
assertTrue(config.isTellFailureIfDeltaIsNegative()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() { |
||||
|
// GIVEN
|
||||
|
var msgData = "{\"pulseCounter\": 42}"; |
||||
|
var msg = TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other")); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() { |
||||
|
// GIVEN
|
||||
|
var msgData = "[]"; |
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other")); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Test |
||||
|
public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() { |
||||
|
// GIVEN
|
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "{}"); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other")); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenDoubleValue_whenOnMsgAndCachingOff_thenShouldTellSuccess() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setRound(1); |
||||
|
config.setInputValueKey("temperature"); |
||||
|
config.setOutputValueKey("temp_delta"); |
||||
|
config.setUseCache(false); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 40.5))); |
||||
|
|
||||
|
var msgData = "{\"temperature\": 42,\"airPressure\":123}"; |
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":1.5}"; |
||||
|
|
||||
|
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenLongStringValue_whenOnMsgAndCachingOff_thenShouldTellSuccess() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setInputValueKey("temperature"); |
||||
|
config.setOutputValueKey("temp_delta"); |
||||
|
config.setUseCache(false); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("temperature", 40L))); |
||||
|
|
||||
|
var msgData = "{\"temperature\": 42,\"airPressure\":123}"; |
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":2}"; |
||||
|
|
||||
|
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenValidStringValue_whenOnMsgAndCachingOff_thenShouldTellSuccess() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setInputValueKey("temperature"); |
||||
|
config.setOutputValueKey("temp_delta"); |
||||
|
config.setUseCache(false); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("temperature", "40.0"))); |
||||
|
|
||||
|
var msgData = "{\"temperature\": 42,\"airPressure\":123}"; |
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":2}"; |
||||
|
|
||||
|
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenTwoMessagesAndPeriodOnAndCachingOn_whenOnMsg_thenVerify() throws TbNodeException { |
||||
|
// STAGE 1
|
||||
|
// GIVEN
|
||||
|
config.setInputValueKey("temperature"); |
||||
|
config.setOutputValueKey("temp_delta"); |
||||
|
config.setPeriodValueKey("ts_delta"); |
||||
|
config.setAddPeriodBetweenMsgs(true); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
mockFindLatest(new BasicTsKvEntry(1L, new DoubleDataEntry("temperature", 40.0))); |
||||
|
|
||||
|
var msgData = "{\"temperature\": 42,\"airPressure\":123}"; |
||||
|
var firstMsgMetaData = new TbMsgMetaData(); |
||||
|
firstMsgMetaData.putValue("ts", String.valueOf(3L)); |
||||
|
var firstMsg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, firstMsgMetaData, msgData); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, firstMsg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":2,\"ts_delta\":2}"; |
||||
|
|
||||
|
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); |
||||
|
|
||||
|
// STAGE 2
|
||||
|
// GIVEN
|
||||
|
reset(ctxMock); |
||||
|
reset(timeseriesServiceMock); |
||||
|
|
||||
|
var secondMsgMetaData = new TbMsgMetaData(); |
||||
|
secondMsgMetaData.putValue("ts", String.valueOf(6L)); |
||||
|
var secondMsg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, secondMsgMetaData, msgData); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, secondMsg); |
||||
|
|
||||
|
// THEN
|
||||
|
actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(timeseriesServiceMock, never()).findLatest(any(), any(), anyList()); |
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":0,\"ts_delta\":3}"; |
||||
|
|
||||
|
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenLastValueIsNull_whenOnMsgAndCachingOff_thenDeltaShouldBeZero() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setInputValueKey("temperature"); |
||||
|
config.setOutputValueKey("temp_delta"); |
||||
|
config.setUseCache(false); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", null))); |
||||
|
|
||||
|
var msgData = "{\"temperature\": 42,\"airPressure\":123}"; |
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":0}"; |
||||
|
|
||||
|
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenNegativeDeltaAndTellFailureIfNegativeDeltaTrue_whenOnMsg_thenShouldTellFailure() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setTellFailureIfDeltaIsNegative(true); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L))); |
||||
|
|
||||
|
var msgData = "{\"pulseCounter\":\"123\"}"; |
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
var actualExceptionCaptor = ArgumentCaptor.forClass(Exception.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellFailure(actualMsgCaptor.capture(), actualExceptionCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
|
||||
|
var expectedExceptionMsg = "Delta value is negative!"; |
||||
|
var actualException = actualExceptionCaptor.getValue(); |
||||
|
|
||||
|
assertEquals(msg, actualMsgCaptor.getValue()); |
||||
|
assertInstanceOf(IllegalArgumentException.class, actualException); |
||||
|
assertEquals(expectedExceptionMsg, actualException.getMessage()); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenInvalidStringValue_whenOnMsg_thenException() { |
||||
|
// GIVEN
|
||||
|
mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high"))); |
||||
|
|
||||
|
var msgData = "{\"pulseCounter\":\"123\"}"; |
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN-THEN
|
||||
|
Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Calculation failed. Unable to parse value [high] of telemetry [pulseCounter] to Double"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenBooleanValue_whenOnMsg_thenException() { |
||||
|
// GIVEN
|
||||
|
mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false))); |
||||
|
|
||||
|
var msgData = "{\"pulseCounter\":true}"; |
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN-THEN
|
||||
|
Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Calculation failed. Boolean values are not supported!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenJsonValue_whenOnMsg_thenException() { |
||||
|
// GIVEN
|
||||
|
mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}"))); |
||||
|
|
||||
|
var msgData = "{\"pulseCounter\":{\"isActive\":true}}"; |
||||
|
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); |
||||
|
|
||||
|
// WHEN-THEN
|
||||
|
Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Calculation failed. JSON values are not supported!"); |
||||
|
} |
||||
|
|
||||
|
private void mockFindLatest(TsKvEntry tsKvEntry) { |
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(timeseriesServiceMock.findLatestSync( |
||||
|
eq(TENANT_ID), eq(DUMMY_DEVICE_ORIGINATOR), argThat(new ListMatcher<>(List.of(tsKvEntry.getKey()))) |
||||
|
)).thenReturn(List.of(tsKvEntry)); |
||||
|
} |
||||
|
|
||||
|
private void mockFindLatestAsync(TsKvEntry tsKvEntry) { |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(timeseriesServiceMock.findLatest( |
||||
|
eq(TENANT_ID), eq(DUMMY_DEVICE_ORIGINATOR), argThat(new ListMatcher<>(List.of(tsKvEntry.getKey()))) |
||||
|
)).thenReturn(Futures.immediateFuture(List.of(tsKvEntry))); |
||||
|
} |
||||
|
|
||||
|
@RequiredArgsConstructor |
||||
|
private static class ListMatcher<T> implements ArgumentMatcher<List<T>> { |
||||
|
|
||||
|
private final List<T> expectedList; |
||||
|
|
||||
|
@Override |
||||
|
public boolean matches(List<T> actualList) { |
||||
|
if (actualList == expectedList) { |
||||
|
return true; |
||||
|
} |
||||
|
if (actualList.size() != expectedList.size()) { |
||||
|
return false; |
||||
|
} |
||||
|
return actualList.containsAll(expectedList); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,368 +0,0 @@ |
|||||
/** |
|
||||
* Copyright © 2016-2023 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.metadata; |
|
||||
|
|
||||
import com.datastax.oss.driver.api.core.uuid.Uuids; |
|
||||
import com.fasterxml.jackson.databind.JsonNode; |
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|
||||
import com.google.common.util.concurrent.Futures; |
|
||||
import org.junit.After; |
|
||||
import org.junit.Assert; |
|
||||
import org.junit.Before; |
|
||||
import org.junit.Test; |
|
||||
import org.junit.runner.RunWith; |
|
||||
import org.mockito.ArgumentCaptor; |
|
||||
import org.mockito.Mock; |
|
||||
import org.mockito.Mockito; |
|
||||
import org.mockito.junit.MockitoJUnitRunner; |
|
||||
import org.thingsboard.common.util.AbstractListeningExecutor; |
|
||||
import org.thingsboard.common.util.JacksonUtil; |
|
||||
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.DataConstants; |
|
||||
import org.thingsboard.server.common.data.id.DeviceId; |
|
||||
import org.thingsboard.server.common.data.id.EntityId; |
|
||||
import org.thingsboard.server.common.data.id.TenantId; |
|
||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
|
||||
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; |
|
||||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|
||||
import org.thingsboard.server.common.data.kv.JsonDataEntry; |
|
||||
import org.thingsboard.server.common.data.kv.StringDataEntry; |
|
||||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|
||||
import org.thingsboard.server.common.msg.TbMsg; |
|
||||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|
||||
import org.thingsboard.server.dao.attributes.AttributesService; |
|
||||
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
|
||||
|
|
||||
import java.util.ArrayList; |
|
||||
import java.util.List; |
|
||||
import java.util.stream.Collectors; |
|
||||
|
|
||||
import static org.mockito.ArgumentMatchers.any; |
|
||||
import static org.mockito.Mockito.lenient; |
|
||||
import static org.mockito.Mockito.never; |
|
||||
|
|
||||
@RunWith(MockitoJUnitRunner.class) |
|
||||
public class TbAbstractGetAttributesNodeTest { |
|
||||
|
|
||||
private EntityId originator = new DeviceId(Uuids.timeBased()); |
|
||||
private TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); |
|
||||
|
|
||||
@Mock |
|
||||
private TbContext ctx; |
|
||||
@Mock |
|
||||
private AttributesService attributesService; |
|
||||
@Mock |
|
||||
private TimeseriesService tsService; |
|
||||
private AbstractListeningExecutor dbExecutor; |
|
||||
|
|
||||
private List<String> clientAttributes; |
|
||||
private List<String> serverAttributes; |
|
||||
private List<String> sharedAttributes; |
|
||||
private List<String> tsKeys; |
|
||||
private long ts; |
|
||||
|
|
||||
@Before |
|
||||
public void before() throws TbNodeException { |
|
||||
dbExecutor = new AbstractListeningExecutor() { |
|
||||
@Override |
|
||||
protected int getThreadPollSize() { |
|
||||
return 3; |
|
||||
} |
|
||||
}; |
|
||||
dbExecutor.init(); |
|
||||
|
|
||||
Mockito.reset(ctx); |
|
||||
Mockito.reset(attributesService); |
|
||||
Mockito.reset(tsService); |
|
||||
|
|
||||
Mockito.reset(ctx); |
|
||||
Mockito.reset(attributesService); |
|
||||
Mockito.reset(tsService); |
|
||||
|
|
||||
lenient().when(ctx.getAttributesService()).thenReturn(attributesService); |
|
||||
lenient().when(ctx.getTimeseriesService()).thenReturn(tsService); |
|
||||
lenient().when(ctx.getTenantId()).thenReturn(tenantId); |
|
||||
lenient().when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); |
|
||||
|
|
||||
clientAttributes = getAttributeNames("client"); |
|
||||
serverAttributes = getAttributeNames("server"); |
|
||||
sharedAttributes = getAttributeNames("shared"); |
|
||||
tsKeys = List.of("temperature", "humidity", "unknown"); |
|
||||
ts = System.currentTimeMillis(); |
|
||||
|
|
||||
Mockito.when(attributesService.find(tenantId, originator, DataConstants.CLIENT_SCOPE, clientAttributes)) |
|
||||
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(clientAttributes, ts))); |
|
||||
|
|
||||
|
|
||||
Mockito.when(attributesService.find(tenantId, originator, DataConstants.SERVER_SCOPE, serverAttributes)) |
|
||||
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(serverAttributes, ts))); |
|
||||
|
|
||||
|
|
||||
Mockito.when(attributesService.find(tenantId, originator, DataConstants.SHARED_SCOPE, sharedAttributes)) |
|
||||
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(sharedAttributes, ts))); |
|
||||
|
|
||||
Mockito.when(tsService.findLatest(tenantId, originator, tsKeys)) |
|
||||
.thenReturn(Futures.immediateFuture(getListTsKvEntry(tsKeys, ts))); |
|
||||
} |
|
||||
|
|
||||
@After |
|
||||
public void after() { |
|
||||
dbExecutor.destroy(); |
|
||||
} |
|
||||
|
|
||||
@Test |
|
||||
public void fetchToMetadata_whenOnMsg_then_success() throws Exception { |
|
||||
TbGetAttributesNode node = initNode(false, false, false); |
|
||||
TbMsg msg = getTbMsg(originator); |
|
||||
node.onMsg(ctx, msg); |
|
||||
|
|
||||
// check msg
|
|
||||
TbMsg resultMsg = checkMsg(true); |
|
||||
|
|
||||
//check attributes
|
|
||||
checkAttributes(resultMsg, false, "cs_", clientAttributes); |
|
||||
checkAttributes(resultMsg, false, "ss_", serverAttributes); |
|
||||
checkAttributes(resultMsg, false, "shared_", sharedAttributes); |
|
||||
|
|
||||
//check timeseries
|
|
||||
checkTs(resultMsg, false, false, tsKeys); |
|
||||
} |
|
||||
|
|
||||
@Test |
|
||||
public void fetchToMetadata_latestWithTs_whenOnMsg_then_success() throws Exception { |
|
||||
TbGetAttributesNode node = initNode(false, true, false); |
|
||||
TbMsg msg = getTbMsg(originator); |
|
||||
node.onMsg(ctx, msg); |
|
||||
|
|
||||
// check msg
|
|
||||
TbMsg resultMsg = checkMsg(true); |
|
||||
|
|
||||
//check attributes
|
|
||||
checkAttributes(resultMsg, false, "cs_", clientAttributes); |
|
||||
checkAttributes(resultMsg, false, "ss_", serverAttributes); |
|
||||
checkAttributes(resultMsg, false, "shared_", sharedAttributes); |
|
||||
|
|
||||
//check timeseries with ts
|
|
||||
checkTs(resultMsg, false, true, tsKeys); |
|
||||
} |
|
||||
|
|
||||
@Test |
|
||||
public void fetchToData_whenOnMsg_then_success() throws Exception { |
|
||||
TbGetAttributesNode node = initNode(true, false, false); |
|
||||
TbMsg msg = getTbMsg(originator); |
|
||||
node.onMsg(ctx, msg); |
|
||||
|
|
||||
// check msg
|
|
||||
TbMsg resultMsg = checkMsg(true); |
|
||||
|
|
||||
//check attributes
|
|
||||
checkAttributes(resultMsg, true, "cs_", clientAttributes); |
|
||||
checkAttributes(resultMsg, true, "ss_", serverAttributes); |
|
||||
checkAttributes(resultMsg, true, "shared_", sharedAttributes); |
|
||||
|
|
||||
//check timeseries
|
|
||||
checkTs(resultMsg, true, false, tsKeys); |
|
||||
} |
|
||||
|
|
||||
@Test |
|
||||
public void fetchToData_latestWithTs_whenOnMsg_then_success() throws Exception { |
|
||||
TbGetAttributesNode node = initNode(true, true, false); |
|
||||
TbMsg msg = getTbMsg(originator); |
|
||||
node.onMsg(ctx, msg); |
|
||||
|
|
||||
// check msg
|
|
||||
TbMsg resultMsg = checkMsg(true); |
|
||||
|
|
||||
//check attributes
|
|
||||
checkAttributes(resultMsg, true, "cs_", clientAttributes); |
|
||||
checkAttributes(resultMsg, true, "ss_", serverAttributes); |
|
||||
checkAttributes(resultMsg, true, "shared_", sharedAttributes); |
|
||||
|
|
||||
//check timeseries with ts
|
|
||||
checkTs(resultMsg, true, true, tsKeys); |
|
||||
} |
|
||||
|
|
||||
@Test |
|
||||
public void fetchToMetadata_whenOnMsg_then_failure() throws Exception { |
|
||||
TbGetAttributesNode node = initNode(false, false, true); |
|
||||
TbMsg msg = getTbMsg(originator); |
|
||||
node.onMsg(ctx, msg); |
|
||||
|
|
||||
// check msg
|
|
||||
TbMsg actualMsg = checkMsg(false); |
|
||||
|
|
||||
//check attributes
|
|
||||
checkAttributes(actualMsg, false, "cs_", clientAttributes); |
|
||||
checkAttributes(actualMsg, false, "ss_", serverAttributes); |
|
||||
checkAttributes(actualMsg, false, "shared_", sharedAttributes); |
|
||||
|
|
||||
//check timeseries with ts
|
|
||||
checkTs(actualMsg, false, false, tsKeys); |
|
||||
} |
|
||||
|
|
||||
@Test |
|
||||
public void fetchToData_whenOnMsg_then_failure() throws Exception { |
|
||||
TbGetAttributesNode node = initNode(true, true, true); |
|
||||
TbMsg msg = getTbMsg(originator); |
|
||||
node.onMsg(ctx, msg); |
|
||||
|
|
||||
// check msg
|
|
||||
TbMsg actualMsg = checkMsg(false); |
|
||||
|
|
||||
//check attributes
|
|
||||
checkAttributes(actualMsg, true, "cs_", clientAttributes); |
|
||||
checkAttributes(actualMsg, true, "ss_", serverAttributes); |
|
||||
checkAttributes(actualMsg, true, "shared_", sharedAttributes); |
|
||||
|
|
||||
//check timeseries with ts
|
|
||||
checkTs(actualMsg, true, true, tsKeys); |
|
||||
} |
|
||||
|
|
||||
@Test |
|
||||
public void fetchToData_whenOnMsg_and_data_is_not_object_then_failure() throws Exception { |
|
||||
TbGetAttributesNode node = initNode(true, true, true); |
|
||||
TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), "[]"); |
|
||||
node.onMsg(ctx, msg); |
|
||||
|
|
||||
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
|
||||
ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(IllegalArgumentException.class); |
|
||||
Mockito.verify(ctx, never()).tellSuccess(any()); |
|
||||
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture()); |
|
||||
|
|
||||
Assert.assertSame(msg, newMsgCaptor.getValue()); |
|
||||
Assert.assertNotNull(exceptionCaptor.getValue()); |
|
||||
} |
|
||||
|
|
||||
private TbMsg checkMsg(boolean checkSuccess) { |
|
||||
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
|
||||
if (checkSuccess) { |
|
||||
Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); |
|
||||
} else { |
|
||||
ArgumentCaptor<RuntimeException> exceptionCaptor = ArgumentCaptor.forClass(RuntimeException.class); |
|
||||
Mockito.verify(ctx, never()).tellSuccess(any()); |
|
||||
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(msgCaptor.capture(), exceptionCaptor.capture()); |
|
||||
RuntimeException exception = exceptionCaptor.getValue(); |
|
||||
Assert.assertNotNull(exception); |
|
||||
Assert.assertNotNull(exception.getMessage()); |
|
||||
Assert.assertTrue(exception.getMessage().startsWith("The following attribute/telemetry keys is not present in the DB:")); |
|
||||
} |
|
||||
|
|
||||
TbMsg resultMsg = msgCaptor.getValue(); |
|
||||
Assert.assertNotNull(resultMsg); |
|
||||
Assert.assertNotNull(resultMsg.getMetaData()); |
|
||||
Assert.assertNotNull(resultMsg.getData()); |
|
||||
return resultMsg; |
|
||||
} |
|
||||
|
|
||||
private void checkAttributes(TbMsg actualMsg, boolean fetchToData, String prefix, List<String> attributes) { |
|
||||
JsonNode msgData = JacksonUtil.toJsonNode(actualMsg.getData()); |
|
||||
attributes.stream() |
|
||||
.filter(attribute -> !attribute.equals("unknown")) |
|
||||
.forEach(attribute -> { |
|
||||
String result; |
|
||||
if (fetchToData) { |
|
||||
result = msgData.get(prefix + attribute).asText(); |
|
||||
} else { |
|
||||
result = actualMsg.getMetaData().getValue(prefix + attribute); |
|
||||
} |
|
||||
Assert.assertNotNull(result); |
|
||||
Assert.assertEquals(attribute + "_value", result); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private void checkTs(TbMsg actualMsg, boolean fetchToData, boolean getLatestValueWithTs, List<String> tsKeys) { |
|
||||
JsonNode msgData = JacksonUtil.toJsonNode(actualMsg.getData()); |
|
||||
long value = 1L; |
|
||||
for (String key : tsKeys) { |
|
||||
if (key.equals("unknown")) { |
|
||||
continue; |
|
||||
} |
|
||||
String actualValue; |
|
||||
String expectedValue; |
|
||||
if (getLatestValueWithTs) { |
|
||||
expectedValue = "{\"ts\":" + ts + ",\"value\":{\"data\":" + value + "}}"; |
|
||||
} else { |
|
||||
expectedValue = "{\"data\":" + value + "}"; |
|
||||
} |
|
||||
if (fetchToData) { |
|
||||
actualValue = JacksonUtil.toString(msgData.get(key)); |
|
||||
} else { |
|
||||
actualValue = actualMsg.getMetaData().getValue(key); |
|
||||
} |
|
||||
Assert.assertNotNull(actualValue); |
|
||||
Assert.assertEquals(expectedValue, actualValue); |
|
||||
value++; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private TbGetAttributesNode initNode(boolean fetchToData, boolean getLatestValueWithTs, boolean isTellFailureIfAbsent) throws TbNodeException { |
|
||||
TbGetAttributesNodeConfiguration config = new TbGetAttributesNodeConfiguration(); |
|
||||
config.setClientAttributeNames(List.of("client_attr_1", "client_attr_2", "${client_attr_metadata}", "unknown")); |
|
||||
config.setServerAttributeNames(List.of("server_attr_1", "server_attr_2", "${server_attr_metadata}", "unknown")); |
|
||||
config.setSharedAttributeNames(List.of("shared_attr_1", "shared_attr_2", "$[shared_attr_data]", "unknown")); |
|
||||
config.setLatestTsKeyNames(List.of("temperature", "humidity", "unknown")); |
|
||||
config.setFetchToData(fetchToData); |
|
||||
config.setGetLatestValueWithTs(getLatestValueWithTs); |
|
||||
config.setTellFailureIfAbsent(isTellFailureIfAbsent); |
|
||||
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|
||||
TbGetAttributesNode node = new TbGetAttributesNode(); |
|
||||
node.init(ctx, nodeConfiguration); |
|
||||
return node; |
|
||||
} |
|
||||
|
|
||||
private TbMsg getTbMsg(EntityId entityId) { |
|
||||
ObjectNode msgData = JacksonUtil.newObjectNode(); |
|
||||
msgData.put("shared_attr_data", "shared_attr_3"); |
|
||||
|
|
||||
TbMsgMetaData msgMetaData = new TbMsgMetaData(); |
|
||||
msgMetaData.putValue("client_attr_metadata", "client_attr_3"); |
|
||||
msgMetaData.putValue("server_attr_metadata", "server_attr_3"); |
|
||||
|
|
||||
return TbMsg.newMsg("TEST", entityId, msgMetaData, JacksonUtil.toString(msgData)); |
|
||||
} |
|
||||
|
|
||||
private List<String> getAttributeNames(String prefix) { |
|
||||
return List.of(prefix + "_attr_1", prefix + "_attr_2", prefix + "_attr_3", "unknown"); |
|
||||
} |
|
||||
|
|
||||
private List<AttributeKvEntry> getListAttributeKvEntry(List<String> attributes, long ts) { |
|
||||
return attributes.stream() |
|
||||
.filter(attribute -> !attribute.equals("unknown")) |
|
||||
.map(attribute -> toAttributeKvEntry(ts, attribute)) |
|
||||
.collect(Collectors.toList()); |
|
||||
} |
|
||||
|
|
||||
private BaseAttributeKvEntry toAttributeKvEntry(long ts, String attribute) { |
|
||||
return new BaseAttributeKvEntry(ts, new StringDataEntry(attribute, attribute + "_value")); |
|
||||
} |
|
||||
|
|
||||
private List<TsKvEntry> getListTsKvEntry(List<String> keys, long ts) { |
|
||||
long value = 1L; |
|
||||
List<TsKvEntry> kvEntries = new ArrayList<>(); |
|
||||
for (String key : keys) { |
|
||||
if (key.equals("unknown")) { |
|
||||
continue; |
|
||||
} |
|
||||
String dataValue = "{\"data\":" + value + "}"; |
|
||||
kvEntries.add(new BasicTsKvEntry(ts, new JsonDataEntry(key, dataValue))); |
|
||||
value++; |
|
||||
} |
|
||||
return kvEntries; |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,388 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import com.datastax.oss.driver.api.core.uuid.Uuids; |
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import org.junit.After; |
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.junit.jupiter.api.Assertions; |
||||
|
import org.junit.runner.RunWith; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
import org.mockito.Mock; |
||||
|
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
import org.thingsboard.common.util.AbstractListeningExecutor; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
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.DataConstants; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.JsonDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.dao.attributes.AttributesService; |
||||
|
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.junit.Assert.assertEquals; |
||||
|
import static org.junit.Assert.assertNotNull; |
||||
|
import static org.junit.Assert.assertTrue; |
||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.Mockito.never; |
||||
|
import static org.mockito.Mockito.timeout; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@RunWith(MockitoJUnitRunner.class) |
||||
|
public class TbGetAttributesNodeTest { |
||||
|
|
||||
|
private static final EntityId ORIGINATOR = new DeviceId(Uuids.timeBased()); |
||||
|
private static final TenantId TENANT_ID = TenantId.fromUUID(Uuids.timeBased()); |
||||
|
private AbstractListeningExecutor dbExecutor; |
||||
|
|
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private AttributesService attributesServiceMock; |
||||
|
@Mock |
||||
|
private TimeseriesService timeseriesServiceMock; |
||||
|
|
||||
|
private List<String> clientAttributes; |
||||
|
private List<String> serverAttributes; |
||||
|
private List<String> sharedAttributes; |
||||
|
private List<String> tsKeys; |
||||
|
private long ts; |
||||
|
private TbGetAttributesNode node; |
||||
|
|
||||
|
@Before |
||||
|
public void before() throws TbNodeException { |
||||
|
dbExecutor = new AbstractListeningExecutor() { |
||||
|
@Override |
||||
|
protected int getThreadPollSize() { |
||||
|
return 3; |
||||
|
} |
||||
|
}; |
||||
|
dbExecutor.init(); |
||||
|
|
||||
|
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); |
||||
|
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock); |
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); |
||||
|
|
||||
|
clientAttributes = getAttributeNames("client"); |
||||
|
serverAttributes = getAttributeNames("server"); |
||||
|
sharedAttributes = getAttributeNames("shared"); |
||||
|
tsKeys = List.of("temperature", "humidity", "unknown"); |
||||
|
ts = System.currentTimeMillis(); |
||||
|
|
||||
|
when(attributesServiceMock.find(TENANT_ID, ORIGINATOR, DataConstants.CLIENT_SCOPE, clientAttributes)) |
||||
|
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(clientAttributes, ts))); |
||||
|
|
||||
|
when(attributesServiceMock.find(TENANT_ID, ORIGINATOR, DataConstants.SERVER_SCOPE, serverAttributes)) |
||||
|
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(serverAttributes, ts))); |
||||
|
|
||||
|
when(attributesServiceMock.find(TENANT_ID, ORIGINATOR, DataConstants.SHARED_SCOPE, sharedAttributes)) |
||||
|
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(sharedAttributes, ts))); |
||||
|
|
||||
|
when(timeseriesServiceMock.findLatest(TENANT_ID, ORIGINATOR, tsKeys)) |
||||
|
.thenReturn(Futures.immediateFuture(getListTsKvEntry(tsKeys, ts))); |
||||
|
} |
||||
|
|
||||
|
@After |
||||
|
public void after() { |
||||
|
dbExecutor.destroy(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenFetchAttributesToMetadata_whenOnMsg_thenShouldTellSuccess() throws Exception { |
||||
|
// GIVEN
|
||||
|
node = initNode(FetchTo.METADATA, false, false); |
||||
|
var msg = getTbMsg(ORIGINATOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var resultMsg = checkMsg(true); |
||||
|
|
||||
|
checkAttributes(resultMsg, FetchTo.METADATA, "cs_", clientAttributes); |
||||
|
checkAttributes(resultMsg, FetchTo.METADATA, "ss_", serverAttributes); |
||||
|
checkAttributes(resultMsg, FetchTo.METADATA, "shared_", sharedAttributes); |
||||
|
|
||||
|
checkTs(resultMsg, FetchTo.METADATA, false, tsKeys); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenFetchLatestTimeseriesToMetadata_whenOnMsg_thenShouldTellSuccess() throws Exception { |
||||
|
// GIVEN
|
||||
|
node = initNode(FetchTo.METADATA, true, false); |
||||
|
var msg = getTbMsg(ORIGINATOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var resultMsg = checkMsg(true); |
||||
|
|
||||
|
checkAttributes(resultMsg, FetchTo.METADATA, "cs_", clientAttributes); |
||||
|
checkAttributes(resultMsg, FetchTo.METADATA, "ss_", serverAttributes); |
||||
|
checkAttributes(resultMsg, FetchTo.METADATA, "shared_", sharedAttributes); |
||||
|
|
||||
|
checkTs(resultMsg, FetchTo.METADATA, true, tsKeys); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenFetchAttributesToData_whenOnMsg_thenShouldTellSuccess() throws Exception { |
||||
|
// GIVEN
|
||||
|
node = initNode(FetchTo.DATA, false, false); |
||||
|
var msg = getTbMsg(ORIGINATOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var resultMsg = checkMsg(true); |
||||
|
|
||||
|
checkAttributes(resultMsg, FetchTo.DATA, "cs_", clientAttributes); |
||||
|
checkAttributes(resultMsg, FetchTo.DATA, "ss_", serverAttributes); |
||||
|
checkAttributes(resultMsg, FetchTo.DATA, "shared_", sharedAttributes); |
||||
|
|
||||
|
checkTs(resultMsg, FetchTo.DATA, false, tsKeys); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenFetchLatestTimeseriesToData_whenOnMsg_thenShouldTellSuccess() throws Exception { |
||||
|
// GIVEN
|
||||
|
node = initNode(FetchTo.DATA, true, false); |
||||
|
var msg = getTbMsg(ORIGINATOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var resultMsg = checkMsg(true); |
||||
|
|
||||
|
checkAttributes(resultMsg, FetchTo.DATA, "cs_", clientAttributes); |
||||
|
checkAttributes(resultMsg, FetchTo.DATA, "ss_", serverAttributes); |
||||
|
checkAttributes(resultMsg, FetchTo.DATA, "shared_", sharedAttributes); |
||||
|
|
||||
|
checkTs(resultMsg, FetchTo.DATA, true, tsKeys); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenFetchAttributesToMetadata_whenOnMsg_thenShouldTellFailure() throws Exception { |
||||
|
// GIVEN
|
||||
|
node = initNode(FetchTo.METADATA, false, true); |
||||
|
var msg = getTbMsg(ORIGINATOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMsg = checkMsg(false); |
||||
|
|
||||
|
checkAttributes(actualMsg, FetchTo.METADATA, "cs_", clientAttributes); |
||||
|
checkAttributes(actualMsg, FetchTo.METADATA, "ss_", serverAttributes); |
||||
|
checkAttributes(actualMsg, FetchTo.METADATA, "shared_", sharedAttributes); |
||||
|
|
||||
|
checkTs(actualMsg, FetchTo.METADATA, false, tsKeys); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenFetchLatestTimeseriesToData_whenOnMsg_thenShouldTellFailure() throws Exception { |
||||
|
// GIVEN
|
||||
|
node = initNode(FetchTo.DATA, true, true); |
||||
|
var msg = getTbMsg(ORIGINATOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMsg = checkMsg(false); |
||||
|
|
||||
|
checkAttributes(actualMsg, FetchTo.DATA, "cs_", clientAttributes); |
||||
|
checkAttributes(actualMsg, FetchTo.DATA, "ss_", serverAttributes); |
||||
|
checkAttributes(actualMsg, FetchTo.DATA, "shared_", sharedAttributes); |
||||
|
|
||||
|
checkTs(actualMsg, FetchTo.DATA, true, tsKeys); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenFetchLatestTimeseriesToDataAndDataIsNotJsonObject_whenOnMsg_thenException() throws Exception { |
||||
|
// GIVEN
|
||||
|
node = initNode(FetchTo.DATA, true, true); |
||||
|
var msg = TbMsg.newMsg("TEST", ORIGINATOR, new TbMsgMetaData(), "[]"); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); |
||||
|
|
||||
|
// THEN
|
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { |
||||
|
var defaultConfig = new TbGetAttributesNodeConfiguration().defaultConfiguration(); |
||||
|
var node = new TbGetAttributesNode(); |
||||
|
String oldConfig = "{\"fetchToData\":false," + |
||||
|
"\"clientAttributeNames\":[]," + |
||||
|
"\"sharedAttributeNames\":[]," + |
||||
|
"\"serverAttributeNames\":[]," + |
||||
|
"\"latestTsKeyNames\":[]," + |
||||
|
"\"tellFailureIfAbsent\":true," + |
||||
|
"\"getLatestValueWithTs\":false}"; |
||||
|
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); |
||||
|
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson); |
||||
|
Assertions.assertTrue(upgrade.getFirst()); |
||||
|
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); |
||||
|
} |
||||
|
|
||||
|
private TbMsg checkMsg(boolean checkSuccess) { |
||||
|
var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
if (checkSuccess) { |
||||
|
verify(ctxMock, timeout(5000)).tellSuccess(msgCaptor.capture()); |
||||
|
} else { |
||||
|
var exceptionCaptor = ArgumentCaptor.forClass(RuntimeException.class); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
verify(ctxMock, timeout(5000)).tellFailure(msgCaptor.capture(), exceptionCaptor.capture()); |
||||
|
var exception = exceptionCaptor.getValue(); |
||||
|
assertNotNull(exception); |
||||
|
assertNotNull(exception.getMessage()); |
||||
|
assertTrue(exception.getMessage().startsWith("The following attribute/telemetry keys is not present in the DB:")); |
||||
|
} |
||||
|
|
||||
|
var resultMsg = msgCaptor.getValue(); |
||||
|
assertNotNull(resultMsg); |
||||
|
assertNotNull(resultMsg.getMetaData()); |
||||
|
assertNotNull(resultMsg.getData()); |
||||
|
return resultMsg; |
||||
|
} |
||||
|
|
||||
|
private void checkAttributes(TbMsg actualMsg, FetchTo fetchTo, String prefix, List<String> attributes) { |
||||
|
var msgData = JacksonUtil.toJsonNode(actualMsg.getData()); |
||||
|
attributes.stream() |
||||
|
.filter(attribute -> !attribute.equals("unknown")) |
||||
|
.forEach(attribute -> { |
||||
|
String result = null; |
||||
|
if (FetchTo.DATA.equals(fetchTo)) { |
||||
|
result = msgData.get(prefix + attribute).asText(); |
||||
|
} else if (FetchTo.METADATA.equals(fetchTo)) { |
||||
|
result = actualMsg.getMetaData().getValue(prefix + attribute); |
||||
|
} |
||||
|
assertNotNull(result); |
||||
|
assertEquals(attribute + "_value", result); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void checkTs(TbMsg actualMsg, FetchTo fetchTo, boolean getLatestValueWithTs, List<String> tsKeys) { |
||||
|
var msgData = JacksonUtil.toJsonNode(actualMsg.getData()); |
||||
|
long value = 1L; |
||||
|
for (var key : tsKeys) { |
||||
|
if (key.equals("unknown")) { |
||||
|
continue; |
||||
|
} |
||||
|
String actualValue = null; |
||||
|
String expectedValue; |
||||
|
if (getLatestValueWithTs) { |
||||
|
expectedValue = "{\"ts\":" + ts + ",\"value\":{\"data\":" + value + "}}"; |
||||
|
} else { |
||||
|
expectedValue = "{\"data\":" + value + "}"; |
||||
|
} |
||||
|
if (FetchTo.DATA.equals(fetchTo)) { |
||||
|
actualValue = JacksonUtil.toString(msgData.get(key)); |
||||
|
} else if (FetchTo.METADATA.equals(fetchTo)) { |
||||
|
actualValue = actualMsg.getMetaData().getValue(key); |
||||
|
} |
||||
|
assertNotNull(actualValue); |
||||
|
assertEquals(expectedValue, actualValue); |
||||
|
value++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private TbGetAttributesNode initNode(FetchTo fetchTo, boolean getLatestValueWithTs, boolean isTellFailureIfAbsent) throws TbNodeException { |
||||
|
var config = new TbGetAttributesNodeConfiguration(); |
||||
|
config.setClientAttributeNames(List.of("client_attr_1", "client_attr_2", "${client_attr_metadata}", "unknown")); |
||||
|
config.setServerAttributeNames(List.of("server_attr_1", "server_attr_2", "${server_attr_metadata}", "unknown")); |
||||
|
config.setSharedAttributeNames(List.of("shared_attr_1", "shared_attr_2", "$[shared_attr_data]", "unknown")); |
||||
|
config.setLatestTsKeyNames(List.of("temperature", "humidity", "unknown")); |
||||
|
config.setFetchTo(fetchTo); |
||||
|
config.setGetLatestValueWithTs(getLatestValueWithTs); |
||||
|
config.setTellFailureIfAbsent(isTellFailureIfAbsent); |
||||
|
|
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
var node = new TbGetAttributesNode(); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
private TbMsg getTbMsg(EntityId entityId) { |
||||
|
var msgData = JacksonUtil.newObjectNode(); |
||||
|
msgData.put("shared_attr_data", "shared_attr_3"); |
||||
|
|
||||
|
var msgMetaData = new TbMsgMetaData(); |
||||
|
msgMetaData.putValue("client_attr_metadata", "client_attr_3"); |
||||
|
msgMetaData.putValue("server_attr_metadata", "server_attr_3"); |
||||
|
|
||||
|
return TbMsg.newMsg("TEST", entityId, msgMetaData, JacksonUtil.toString(msgData)); |
||||
|
} |
||||
|
|
||||
|
private List<String> getAttributeNames(String prefix) { |
||||
|
return List.of(prefix + "_attr_1", prefix + "_attr_2", prefix + "_attr_3", "unknown"); |
||||
|
} |
||||
|
|
||||
|
private List<AttributeKvEntry> getListAttributeKvEntry(List<String> attributesList, long ts) { |
||||
|
return attributesList.stream() |
||||
|
.filter(attribute -> !attribute.equals("unknown")) |
||||
|
.map(attribute -> toAttributeKvEntry(ts, attribute)) |
||||
|
.collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
private BaseAttributeKvEntry toAttributeKvEntry(long ts, String attribute) { |
||||
|
return new BaseAttributeKvEntry(ts, new StringDataEntry(attribute, attribute + "_value")); |
||||
|
} |
||||
|
|
||||
|
private List<TsKvEntry> getListTsKvEntry(List<String> keysList, long ts) { |
||||
|
long value = 1L; |
||||
|
var kvEntriesList = new ArrayList<TsKvEntry>(); |
||||
|
for (var key : keysList) { |
||||
|
if (key.equals("unknown")) { |
||||
|
continue; |
||||
|
} |
||||
|
String dataValue = "{\"data\":" + value + "}"; |
||||
|
kvEntriesList.add(new BasicTsKvEntry(ts, new JsonDataEntry(key, dataValue))); |
||||
|
value++; |
||||
|
} |
||||
|
return kvEntriesList; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,483 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import org.jetbrains.annotations.NotNull; |
||||
|
import org.junit.jupiter.api.Assertions; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
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.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.util.ContactBasedEntityDetails; |
||||
|
import org.thingsboard.server.common.data.Customer; |
||||
|
import org.thingsboard.server.common.data.Dashboard; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.User; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.edge.Edge; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.DashboardId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EdgeId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.dao.asset.AssetService; |
||||
|
import org.thingsboard.server.dao.customer.CustomerService; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
import org.thingsboard.server.dao.edge.EdgeService; |
||||
|
import org.thingsboard.server.dao.entityview.EntityViewService; |
||||
|
import org.thingsboard.server.dao.user.UserService; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.NoSuchElementException; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.Callable; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.Mockito.never; |
||||
|
import static org.mockito.Mockito.times; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class TbGetCustomerDetailsNodeTest { |
||||
|
|
||||
|
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); |
||||
|
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); |
||||
|
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { |
||||
|
@Override |
||||
|
public <T> ListenableFuture<T> executeAsync(Callable<T> task) { |
||||
|
try { |
||||
|
return Futures.immediateFuture(task.call()); |
||||
|
} catch (Exception e) { |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute(@NotNull Runnable command) { |
||||
|
command.run(); |
||||
|
} |
||||
|
}; |
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private CustomerService customerServiceMock; |
||||
|
@Mock |
||||
|
private DeviceService deviceServiceMock; |
||||
|
@Mock |
||||
|
private AssetService assetServiceMock; |
||||
|
@Mock |
||||
|
private EntityViewService entityViewServiceMock; |
||||
|
@Mock |
||||
|
private UserService userServiceMock; |
||||
|
@Mock |
||||
|
private EdgeService edgeServiceMock; |
||||
|
private TbGetCustomerDetailsNode node; |
||||
|
private TbGetCustomerDetailsNodeConfiguration config; |
||||
|
private TbNodeConfiguration nodeConfiguration; |
||||
|
private TbMsg msg; |
||||
|
private Customer customer; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() { |
||||
|
node = new TbGetCustomerDetailsNode(); |
||||
|
config = new TbGetCustomerDetailsNodeConfiguration().defaultConfiguration(); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
customer = new Customer(); |
||||
|
customer.setId(new CustomerId(UUID.randomUUID())); |
||||
|
customer.setTitle("Customer title"); |
||||
|
customer.setCountry("Customer country"); |
||||
|
customer.setCity("Customer city"); |
||||
|
customer.setState("Customer state"); |
||||
|
customer.setZip("123456"); |
||||
|
customer.setAddress("Customer address 1"); |
||||
|
customer.setAddress2("Customer address 2"); |
||||
|
customer.setPhone("+123456789"); |
||||
|
customer.setEmail("email@tenant.com"); |
||||
|
customer.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":\"Customer description\"}")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenConfigWithNullFetchTo_whenInit_thenException() { |
||||
|
// GIVEN
|
||||
|
config.setDetailsList(List.of(ContactBasedEntityDetails.ID)); |
||||
|
config.setFetchTo(null); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!"); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenDefaultConfig_whenInit_thenOK() { |
||||
|
assertThat(config.getDetailsList()).isEqualTo(Collections.emptyList()); |
||||
|
assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenCustomConfig_whenInit_thenOK() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setDetailsList(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); |
||||
|
config.setFetchTo(FetchTo.METADATA); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(node.config).isEqualTo(config); |
||||
|
assertThat(config.getDetailsList()).isEqualTo(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); |
||||
|
assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA); |
||||
|
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { |
||||
|
// GIVEN
|
||||
|
node.fetchTo = FetchTo.DATA; |
||||
|
msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!"); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenAllEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchAllToData() { |
||||
|
// GIVEN
|
||||
|
var device = new Device(); |
||||
|
device.setId(new DeviceId(UUID.randomUUID())); |
||||
|
device.setCustomerId(customer.getId()); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.values()), device.getId()); |
||||
|
|
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device)); |
||||
|
|
||||
|
mockFindCustomer(); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"," + |
||||
|
"\"customer_id\":\"" + customer.getId() + "\"," + |
||||
|
"\"customer_title\":\"" + customer.getTitle() + "\"," + |
||||
|
"\"customer_country\":\"" + customer.getCountry() + "\"," + |
||||
|
"\"customer_city\":\"" + customer.getCity() + "\"," + |
||||
|
"\"customer_state\":\"" + customer.getState() + "\"," + |
||||
|
"\"customer_zip\":\"" + customer.getZip() + "\"," + |
||||
|
"\"customer_address\":\"" + customer.getAddress() + "\"," + |
||||
|
"\"customer_address2\":\"" + customer.getAddress2() + "\"," + |
||||
|
"\"customer_phone\":\"" + customer.getPhone() + "\"," + |
||||
|
"\"customer_email\":\"" + customer.getEmail() + "\"," + |
||||
|
"\"customer_additionalInfo\":\"" + customer.getAdditionalInfo().get("description").asText() + "\"}"; |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenSomeEntityDetailsAndFetchToMetadata_whenOnMsg_thenShouldTellSuccessAndFetchSomeToMetaData() { |
||||
|
// GIVEN
|
||||
|
var asset = new Asset(); |
||||
|
asset.setId(new AssetId(UUID.randomUUID())); |
||||
|
asset.setCustomerId(customer.getId()); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE), asset.getId()); |
||||
|
|
||||
|
when(ctxMock.getAssetService()).thenReturn(assetServiceMock); |
||||
|
when(assetServiceMock.findAssetByIdAsync(eq(TENANT_ID), eq(asset.getId()))).thenReturn(Futures.immediateFuture(asset)); |
||||
|
|
||||
|
mockFindCustomer(); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgMetaData = new TbMsgMetaData(msg.getMetaData().getData()); |
||||
|
expectedMsgMetaData.putValue("customer_id", customer.getId().getId().toString()); |
||||
|
expectedMsgMetaData.putValue("customer_title", customer.getTitle()); |
||||
|
expectedMsgMetaData.putValue("customer_phone", customer.getPhone()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenNotPresentEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() { |
||||
|
// GIVEN
|
||||
|
customer.setZip(null); |
||||
|
customer.setAddress(null); |
||||
|
customer.setAddress2(null); |
||||
|
|
||||
|
var user = new User(); |
||||
|
user.setId(new UserId(UUID.randomUUID())); |
||||
|
user.setCustomerId(customer.getId()); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), user.getId()); |
||||
|
|
||||
|
when(ctxMock.getUserService()).thenReturn(userServiceMock); |
||||
|
when(userServiceMock.findUserByIdAsync(eq(TENANT_ID), eq(user.getId()))).thenReturn(Futures.immediateFuture(user)); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
mockFindCustomer(); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenDidNotFindCustomer_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() { |
||||
|
// GIVEN
|
||||
|
var edge = new Edge(); |
||||
|
edge.setId(new EdgeId(UUID.randomUUID())); |
||||
|
edge.setCustomerId(customer.getId()); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
|
||||
|
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock); |
||||
|
when(edgeServiceMock.findEdgeByIdAsync(eq(TENANT_ID), eq(edge.getId()))).thenReturn(Futures.immediateFuture(edge)); |
||||
|
|
||||
|
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock); |
||||
|
when(customerServiceMock.findCustomerByIdAsync(eq(TENANT_ID), eq(customer.getId()))).thenReturn(Futures.immediateFuture(null)); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenDidNotFindOriginator_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() { |
||||
|
// GIVEN
|
||||
|
var edge = new Edge(); |
||||
|
edge.setId(new EdgeId(UUID.randomUUID())); |
||||
|
edge.setCustomerId(customer.getId()); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
|
||||
|
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock); |
||||
|
when(edgeServiceMock.findEdgeByIdAsync(eq(TENANT_ID), eq(edge.getId()))).thenReturn(Futures.immediateFuture(null)); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenOriginatorNotAssignedToCustomer_whenOnMsg_thenShouldTellFailureAndFetchNothingToData() { |
||||
|
// GIVEN
|
||||
|
var device = new Device(); |
||||
|
device.setId(new DeviceId(UUID.randomUUID())); |
||||
|
device.setName("Thermostat"); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), device.getId()); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
|
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device)); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
var actualExceptionCaptor = ArgumentCaptor.forClass(Exception.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
|
||||
|
var actualMsg = actualMessageCaptor.getValue(); |
||||
|
var actualException = actualExceptionCaptor.getValue(); |
||||
|
|
||||
|
assertThat(actualMsg.getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMsg.getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
|
||||
|
assertThat(actualException).isInstanceOf(RuntimeException.class); |
||||
|
assertThat(actualException.getMessage()).isEqualTo("Device with name 'Thermostat' is not assigned to Customer!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenNullDescriptionAndAddInfoEntityDetails_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() { |
||||
|
// GIVEN
|
||||
|
customer.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":null}")); |
||||
|
|
||||
|
var device = new Device(); |
||||
|
device.setId(new DeviceId(UUID.randomUUID())); |
||||
|
device.setCustomerId(customer.getId()); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO), device.getId()); |
||||
|
|
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device)); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
mockFindCustomer(); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenUnsupportedEntityType_whenOnMsg_thenShouldTellFailureAndFetchNothingToMetaData() { |
||||
|
// GIVEN
|
||||
|
var dashboard = new Dashboard(); |
||||
|
dashboard.setId(new DashboardId(UUID.randomUUID())); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.STATE), dashboard.getId()); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
var actualExceptionCaptor = ArgumentCaptor.forClass(Exception.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
|
||||
|
var actualMsg = actualMessageCaptor.getValue(); |
||||
|
var actualException = actualExceptionCaptor.getValue(); |
||||
|
|
||||
|
assertThat(actualMsg.getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMsg.getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
|
||||
|
assertThat(actualException).isInstanceOf(NoSuchElementException.class); |
||||
|
assertThat(actualException.getMessage()).isEqualTo("Entity with entityType 'DASHBOARD' is not supported."); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { |
||||
|
var defaultConfig = new TbGetCustomerDetailsNodeConfiguration().defaultConfiguration(); |
||||
|
var node = new TbGetCustomerDetailsNode(); |
||||
|
String oldConfig = "{\"detailsList\":[],\"addToMetadata\":false}"; |
||||
|
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); |
||||
|
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson); |
||||
|
Assertions.assertTrue(upgrade.getFirst()); |
||||
|
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); |
||||
|
} |
||||
|
|
||||
|
private void prepareMsgAndConfig(FetchTo fetchTo, List<ContactBasedEntityDetails> detailsList, EntityId originator) { |
||||
|
config.setDetailsList(detailsList); |
||||
|
config.setFetchTo(fetchTo); |
||||
|
|
||||
|
node.config = config; |
||||
|
node.fetchTo = fetchTo; |
||||
|
|
||||
|
var msgMetaData = new TbMsgMetaData(); |
||||
|
msgMetaData.putValue("metaKey1", "metaValue1"); |
||||
|
msgMetaData.putValue("metaKey2", "metaValue2"); |
||||
|
|
||||
|
var msgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"}"; |
||||
|
|
||||
|
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData); |
||||
|
} |
||||
|
|
||||
|
private void mockFindCustomer() { |
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock); |
||||
|
when(customerServiceMock.findCustomerByIdAsync(eq(TENANT_ID), eq(customer.getId()))).thenReturn(Futures.immediateFuture(customer)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import org.junit.Test; |
||||
|
import org.junit.jupiter.api.Assertions; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
|
||||
|
public class TbGetDeviceAttrNodeTest { |
||||
|
|
||||
|
@Test |
||||
|
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { |
||||
|
var defaultConfig = new TbGetDeviceAttrNodeConfiguration().defaultConfiguration(); |
||||
|
var node = new TbGetDeviceAttrNode(); |
||||
|
String oldConfig = "{\"fetchToData\":false," + |
||||
|
"\"clientAttributeNames\":[]," + |
||||
|
"\"sharedAttributeNames\":[]," + |
||||
|
"\"serverAttributeNames\":[]," + |
||||
|
"\"latestTsKeyNames\":[]," + |
||||
|
"\"tellFailureIfAbsent\":true," + |
||||
|
"\"getLatestValueWithTs\":false," + |
||||
|
"\"deviceRelationsQuery\":{\"direction\":\"FROM\",\"maxLevel\":1,\"relationType\":\"Contains\",\"deviceTypes\":[\"default\"]," + |
||||
|
"\"fetchLastLevelOnly\":false}}"; |
||||
|
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); |
||||
|
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson); |
||||
|
Assertions.assertTrue(upgrade.getFirst()); |
||||
|
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,357 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import org.jetbrains.annotations.NotNull; |
||||
|
import org.junit.jupiter.api.Assertions; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
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.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.id.DashboardId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.Callable; |
||||
|
import java.util.concurrent.ExecutionException; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.Mockito.never; |
||||
|
import static org.mockito.Mockito.times; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class TbGetOriginatorFieldsNodeTest { |
||||
|
|
||||
|
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); |
||||
|
private static final TenantId DUMMY_TENANT_ID = new TenantId(UUID.randomUUID()); |
||||
|
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { |
||||
|
@Override |
||||
|
public <T> ListenableFuture<T> executeAsync(Callable<T> task) { |
||||
|
try { |
||||
|
return Futures.immediateFuture(task.call()); |
||||
|
} catch (Exception e) { |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute(@NotNull Runnable command) { |
||||
|
command.run(); |
||||
|
} |
||||
|
}; |
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private DeviceService deviceServiceMock; |
||||
|
private TbGetOriginatorFieldsNode node; |
||||
|
private TbGetOriginatorFieldsConfiguration config; |
||||
|
private TbNodeConfiguration nodeConfiguration; |
||||
|
private TbMsg msg; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() { |
||||
|
node = new TbGetOriginatorFieldsNode(); |
||||
|
config = new TbGetOriginatorFieldsConfiguration().defaultConfiguration(); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenConfigWithNullFetchTo_whenInit_thenException() { |
||||
|
// GIVEN
|
||||
|
config.setFetchTo(null); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!"); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenDefaultConfig_whenInit_thenOK() throws TbNodeException { |
||||
|
// GIVEN-WHEN
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(node.config).isEqualTo(config); |
||||
|
assertThat(config.getDataMapping()).isEqualTo(Map.of( |
||||
|
"name", "originatorName", |
||||
|
"type", "originatorType")); |
||||
|
assertThat(config.isIgnoreNullStrings()).isEqualTo(false); |
||||
|
assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA); |
||||
|
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenCustomConfig_whenInit_thenOK() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setDataMapping(Map.of( |
||||
|
"email", "originatorEmail", |
||||
|
"title", "originatorTitle", |
||||
|
"country", "originatorCountry")); |
||||
|
config.setIgnoreNullStrings(true); |
||||
|
config.setFetchTo(FetchTo.DATA); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(node.config).isEqualTo(config); |
||||
|
assertThat(config.getDataMapping()).isEqualTo(Map.of( |
||||
|
"email", "originatorEmail", |
||||
|
"title", "originatorTitle", |
||||
|
"country", "originatorCountry")); |
||||
|
assertThat(config.isIgnoreNullStrings()).isEqualTo(true); |
||||
|
assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA); |
||||
|
assertThat(node.fetchTo).isEqualTo(FetchTo.DATA); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { |
||||
|
// GIVEN
|
||||
|
node.fetchTo = FetchTo.DATA; |
||||
|
msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!"); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenValidMsgAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchToData() throws TbNodeException, ExecutionException, InterruptedException { |
||||
|
// GIVEN
|
||||
|
var device = new Device(); |
||||
|
device.setId(DUMMY_DEVICE_ORIGINATOR); |
||||
|
device.setName("Test device"); |
||||
|
device.setType("Test device type"); |
||||
|
|
||||
|
config.setDataMapping(Map.of( |
||||
|
"name", "originatorName", |
||||
|
"type", "originatorType", |
||||
|
"label", "originatorLabel")); |
||||
|
config.setIgnoreNullStrings(true); |
||||
|
config.setFetchTo(FetchTo.DATA); |
||||
|
|
||||
|
node.config = config; |
||||
|
node.fetchTo = FetchTo.DATA; |
||||
|
var msgMetaData = new TbMsgMetaData(); |
||||
|
var msgData = "{\"temp\":42,\"humidity\":77}"; |
||||
|
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); |
||||
|
|
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); |
||||
|
when(deviceServiceMock.findDeviceById(eq(DUMMY_TENANT_ID), eq(device.getId()))).thenReturn(device); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgData = "{\"temp\":42,\"humidity\":77,\"originatorName\":\"Test device\",\"originatorType\":\"Test device type\"}"; |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msgMetaData); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenValidMsgAndFetchToMetaData_whenOnMsg_thenShouldTellSuccessAndFetchToMetaData() throws TbNodeException, ExecutionException, InterruptedException { |
||||
|
// GIVEN
|
||||
|
var device = new Device(); |
||||
|
device.setId(DUMMY_DEVICE_ORIGINATOR); |
||||
|
device.setName("Test device"); |
||||
|
device.setType("Test device type"); |
||||
|
|
||||
|
config.setDataMapping(Map.of( |
||||
|
"name", "originatorName", |
||||
|
"type", "originatorType", |
||||
|
"label", "originatorLabel")); |
||||
|
config.setIgnoreNullStrings(true); |
||||
|
config.setFetchTo(FetchTo.METADATA); |
||||
|
|
||||
|
node.config = config; |
||||
|
node.fetchTo = FetchTo.METADATA; |
||||
|
var msgMetaData = new TbMsgMetaData(Map.of( |
||||
|
"testKey1", "testValue1", |
||||
|
"testKey2", "123")); |
||||
|
var msgData = "[\"value1\",\"value2\"]"; |
||||
|
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); |
||||
|
|
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); |
||||
|
when(deviceServiceMock.findDeviceById(eq(DUMMY_TENANT_ID), eq(device.getId()))).thenReturn(device); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgMetaData = new TbMsgMetaData(Map.of( |
||||
|
"testKey1", "testValue1", |
||||
|
"testKey2", "123", |
||||
|
"originatorName", "Test device", |
||||
|
"originatorType", "Test device type" |
||||
|
)); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msgData); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenNullEntityFieldsAndIgnoreNullStringsFalse_whenOnMsg_thenShouldTellSuccessAndFetchNullField() throws TbNodeException, ExecutionException, InterruptedException { |
||||
|
// GIVEN
|
||||
|
var device = new Device(); |
||||
|
device.setId(DUMMY_DEVICE_ORIGINATOR); |
||||
|
device.setName("Test device"); |
||||
|
device.setType("Test device type"); |
||||
|
|
||||
|
config.setDataMapping(Map.of( |
||||
|
"name", "originatorName", |
||||
|
"type", "originatorType", |
||||
|
"label", "originatorLabel")); |
||||
|
config.setIgnoreNullStrings(false); |
||||
|
config.setFetchTo(FetchTo.METADATA); |
||||
|
|
||||
|
node.config = config; |
||||
|
node.fetchTo = FetchTo.METADATA; |
||||
|
var msgMetaData = new TbMsgMetaData(Map.of( |
||||
|
"testKey1", "testValue1", |
||||
|
"testKey2", "123")); |
||||
|
var msgData = "[\"value1\",\"value2\"]"; |
||||
|
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); |
||||
|
|
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); |
||||
|
when(deviceServiceMock.findDeviceById(eq(DUMMY_TENANT_ID), eq(device.getId()))).thenReturn(device); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgMetaData = new TbMsgMetaData(Map.of( |
||||
|
"testKey1", "testValue1", |
||||
|
"testKey2", "123", |
||||
|
"originatorName", "Test device", |
||||
|
"originatorType", "Test device type", |
||||
|
"originatorLabel", "null" |
||||
|
)); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msgData); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenEmptyFieldsMapping_whenInit_thenException() { |
||||
|
// GIVEN
|
||||
|
config.setDataMapping(Collections.emptyMap()); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(exception.getMessage()).isEqualTo("At least one mapping entry should be specified!"); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenUnsupportedEntityType_whenOnMsg_thenShouldTellFailureWithSameMsg() throws TbNodeException, ExecutionException, InterruptedException { |
||||
|
// GIVEN
|
||||
|
config.setDataMapping(Map.of( |
||||
|
"name", "originatorName", |
||||
|
"type", "originatorType", |
||||
|
"label", "originatorLabel")); |
||||
|
config.setIgnoreNullStrings(false); |
||||
|
config.setFetchTo(FetchTo.METADATA); |
||||
|
|
||||
|
node.config = config; |
||||
|
node.fetchTo = FetchTo.METADATA; |
||||
|
var msgMetaData = new TbMsgMetaData(Map.of( |
||||
|
"testKey1", "testValue1", |
||||
|
"testKey2", "123")); |
||||
|
var msgData = "[\"value1\",\"value2\"]"; |
||||
|
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", new DashboardId(UUID.randomUUID()), msgMetaData, msgData); |
||||
|
|
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
verify(ctxMock, times(1)).tellFailure(actualMessageCaptor.capture(), any()); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msgData); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msgMetaData); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { |
||||
|
var defaultConfig = new TbGetOriginatorFieldsConfiguration().defaultConfiguration(); |
||||
|
var node = new TbGetOriginatorFieldsNode(); |
||||
|
String oldConfig = "{\"fieldsMapping\":{\"name\":\"originatorName\",\"type\":\"originatorType\"},\"ignoreNullStrings\":false}"; |
||||
|
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); |
||||
|
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson); |
||||
|
Assertions.assertTrue(upgrade.getFirst()); |
||||
|
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,298 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.metadata; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import org.junit.jupiter.api.Assertions; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
import org.mockito.Mock; |
||||
|
import org.mockito.junit.jupiter.MockitoExtension; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.util.ContactBasedEntityDetails; |
||||
|
import org.thingsboard.server.common.data.Tenant; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.dao.tenant.TenantService; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.Mockito.never; |
||||
|
import static org.mockito.Mockito.times; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class TbGetTenantDetailsNodeTest { |
||||
|
|
||||
|
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); |
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private TenantService tenantServiceMock; |
||||
|
private TbGetTenantDetailsNode node; |
||||
|
private TbGetTenantDetailsNodeConfiguration config; |
||||
|
private TbNodeConfiguration nodeConfiguration; |
||||
|
private TbMsg msg; |
||||
|
private Tenant tenant; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() { |
||||
|
node = new TbGetTenantDetailsNode(); |
||||
|
config = new TbGetTenantDetailsNodeConfiguration().defaultConfiguration(); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
tenant = new Tenant(); |
||||
|
tenant.setId(new TenantId(UUID.randomUUID())); |
||||
|
tenant.setTitle("Tenant title"); |
||||
|
tenant.setCountry("Tenant country"); |
||||
|
tenant.setCity("Tenant city"); |
||||
|
tenant.setState("Tenant state"); |
||||
|
tenant.setZip("123456"); |
||||
|
tenant.setAddress("Tenant address 1"); |
||||
|
tenant.setAddress2("Tenant address 2"); |
||||
|
tenant.setPhone("+123456789"); |
||||
|
tenant.setEmail("email@tenant.com"); |
||||
|
tenant.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":\"Tenant description\"}")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenConfigWithNullFetchTo_whenInit_thenException() { |
||||
|
// GIVEN
|
||||
|
config.setDetailsList(List.of(ContactBasedEntityDetails.ID)); |
||||
|
config.setFetchTo(null); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!"); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenDefaultConfig_whenInit_thenOK() { |
||||
|
// THEN
|
||||
|
assertThat(config.getDetailsList()).isEqualTo(Collections.emptyList()); |
||||
|
assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenCustomConfig_whenInit_thenOK() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setDetailsList(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); |
||||
|
config.setFetchTo(FetchTo.METADATA); |
||||
|
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(node.config).isEqualTo(config); |
||||
|
assertThat(config.getDetailsList()).isEqualTo(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); |
||||
|
assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA); |
||||
|
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { |
||||
|
// GIVEN
|
||||
|
node.fetchTo = FetchTo.DATA; |
||||
|
msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!"); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenAllEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchAllToData() { |
||||
|
// GIVEN
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.values())); |
||||
|
|
||||
|
mockFindTenant(); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"," + |
||||
|
"\"tenant_id\":\"" + tenant.getId() + "\"," + |
||||
|
"\"tenant_title\":\"" + tenant.getTitle() + "\"," + |
||||
|
"\"tenant_country\":\"" + tenant.getCountry() + "\"," + |
||||
|
"\"tenant_city\":\"" + tenant.getCity() + "\"," + |
||||
|
"\"tenant_state\":\"" + tenant.getState() + "\"," + |
||||
|
"\"tenant_zip\":\"" + tenant.getZip() + "\"," + |
||||
|
"\"tenant_address\":\"" + tenant.getAddress() + "\"," + |
||||
|
"\"tenant_address2\":\"" + tenant.getAddress2() + "\"," + |
||||
|
"\"tenant_phone\":\"" + tenant.getPhone() + "\"," + |
||||
|
"\"tenant_email\":\"" + tenant.getEmail() + "\"," + |
||||
|
"\"tenant_additionalInfo\":\"" + tenant.getAdditionalInfo().get("description").asText() + "\"}"; |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenSomeEntityDetailsAndFetchToMetadata_whenOnMsg_thenShouldTellSuccessAndFetchSomeToMetaData() { |
||||
|
// GIVEN
|
||||
|
prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE)); |
||||
|
|
||||
|
mockFindTenant(); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
var expectedMsgMetaData = new TbMsgMetaData(msg.getMetaData().getData()); |
||||
|
expectedMsgMetaData.putValue("tenant_id", tenant.getId().getId().toString()); |
||||
|
expectedMsgMetaData.putValue("tenant_title", tenant.getTitle()); |
||||
|
expectedMsgMetaData.putValue("tenant_phone", tenant.getPhone()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenNotPresentEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() { |
||||
|
// GIVEN
|
||||
|
tenant.setZip(null); |
||||
|
tenant.setAddress(null); |
||||
|
tenant.setAddress2(null); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); |
||||
|
|
||||
|
mockFindTenant(); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenDidNotFindTenant_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() { |
||||
|
// GIVEN
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(tenant.getId()); |
||||
|
when(ctxMock.getTenantService()).thenReturn(tenantServiceMock); |
||||
|
when(tenantServiceMock.findTenantByIdAsync(eq(tenant.getId()), eq(tenant.getId()))).thenReturn(Futures.immediateFuture(null)); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenNullDescriptionAndAddInfoEntityDetails_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() { |
||||
|
// GIVEN
|
||||
|
tenant.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":null}")); |
||||
|
|
||||
|
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO)); |
||||
|
|
||||
|
mockFindTenant(); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
|
||||
|
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
|
||||
|
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData()); |
||||
|
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { |
||||
|
var defaultConfig = new TbGetTenantDetailsNodeConfiguration().defaultConfiguration(); |
||||
|
var node = new TbGetTenantDetailsNode(); |
||||
|
String oldConfig = "{\"detailsList\":[],\"addToMetadata\":false}"; |
||||
|
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); |
||||
|
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson); |
||||
|
Assertions.assertTrue(upgrade.getFirst()); |
||||
|
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); |
||||
|
} |
||||
|
|
||||
|
private void prepareMsgAndConfig(FetchTo fetchTo, List<ContactBasedEntityDetails> detailsList) { |
||||
|
config.setDetailsList(detailsList); |
||||
|
config.setFetchTo(fetchTo); |
||||
|
|
||||
|
node.config = config; |
||||
|
node.fetchTo = fetchTo; |
||||
|
|
||||
|
var msgMetaData = new TbMsgMetaData(); |
||||
|
msgMetaData.putValue("metaKey1", "metaValue1"); |
||||
|
msgMetaData.putValue("metaKey2", "metaValue2"); |
||||
|
|
||||
|
var msgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"}"; |
||||
|
|
||||
|
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); |
||||
|
} |
||||
|
|
||||
|
private void mockFindTenant() { |
||||
|
when(ctxMock.getTenantId()).thenReturn(tenant.getId()); |
||||
|
when(ctxMock.getTenantService()).thenReturn(tenantServiceMock); |
||||
|
when(tenantServiceMock.findTenantByIdAsync(eq(tenant.getId()), eq(tenant.getId()))).thenReturn(Futures.immediateFuture(tenant)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,170 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.util; |
||||
|
|
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import org.jetbrains.annotations.NotNull; |
||||
|
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.ListeningExecutor; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.Customer; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.User; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.dao.asset.AssetService; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
import org.thingsboard.server.dao.user.UserService; |
||||
|
|
||||
|
import java.util.EnumSet; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.Callable; |
||||
|
import java.util.concurrent.ExecutionException; |
||||
|
|
||||
|
import static org.junit.jupiter.api.Assertions.assertEquals; |
||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf; |
||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.Mockito.doReturn; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class EntitiesCustomerIdAsyncLoaderTest { |
||||
|
|
||||
|
private static final EnumSet<EntityType> SUPPORTED_ENTITY_TYPES = EnumSet.of( |
||||
|
EntityType.CUSTOMER, |
||||
|
EntityType.USER, |
||||
|
EntityType.ASSET, |
||||
|
EntityType.DEVICE |
||||
|
); |
||||
|
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { |
||||
|
@Override |
||||
|
public <T> ListenableFuture<T> executeAsync(Callable<T> task) { |
||||
|
try { |
||||
|
return Futures.immediateFuture(task.call()); |
||||
|
} catch (Exception e) { |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void execute(@NotNull Runnable command) { |
||||
|
command.run(); |
||||
|
} |
||||
|
}; |
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private UserService userServiceMock; |
||||
|
@Mock |
||||
|
private AssetService assetServiceMock; |
||||
|
@Mock |
||||
|
private DeviceService deviceServiceMock; |
||||
|
|
||||
|
@Test |
||||
|
public void givenCustomerEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { |
||||
|
// GIVEN
|
||||
|
var customer = new Customer(new CustomerId(UUID.randomUUID())); |
||||
|
|
||||
|
// WHEN
|
||||
|
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, customer.getId()).get(); |
||||
|
|
||||
|
// THEN
|
||||
|
assertEquals(customer.getId(), actualCustomerId); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenUserEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { |
||||
|
// GIVEN
|
||||
|
var user = new User(new UserId(UUID.randomUUID())); |
||||
|
var expectedCustomerId = new CustomerId(UUID.randomUUID()); |
||||
|
user.setCustomerId(expectedCustomerId); |
||||
|
|
||||
|
when(ctxMock.getUserService()).thenReturn(userServiceMock); |
||||
|
doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(any(), any()); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, user.getId()).get(); |
||||
|
|
||||
|
// THEN
|
||||
|
assertEquals(expectedCustomerId, actualCustomerId); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenAssetEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { |
||||
|
// GIVEN
|
||||
|
var asset = new Asset(new AssetId(UUID.randomUUID())); |
||||
|
var expectedCustomerId = new CustomerId(UUID.randomUUID()); |
||||
|
asset.setCustomerId(expectedCustomerId); |
||||
|
|
||||
|
when(ctxMock.getAssetService()).thenReturn(assetServiceMock); |
||||
|
doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(any(), any()); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, asset.getId()).get(); |
||||
|
|
||||
|
// THEN
|
||||
|
assertEquals(expectedCustomerId, actualCustomerId); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenDeviceEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { |
||||
|
// GIVEN
|
||||
|
var device = new Device(new DeviceId(UUID.randomUUID())); |
||||
|
var expectedCustomerId = new CustomerId(UUID.randomUUID()); |
||||
|
device.setCustomerId(expectedCustomerId); |
||||
|
|
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
doReturn(device).when(deviceServiceMock).findDeviceById(any(), any()); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
// WHEN
|
||||
|
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, device.getId()).get(); |
||||
|
|
||||
|
// THEN
|
||||
|
assertEquals(expectedCustomerId, actualCustomerId); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenUnsupportedEntityTypes_whenFindEntityIdAsync_thenException() { |
||||
|
for (var entityType : EntityType.values()) { |
||||
|
if (!SUPPORTED_ENTITY_TYPES.contains(entityType)) { |
||||
|
var entityId = EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID()); |
||||
|
|
||||
|
var expectedExceptionMsg = "org.thingsboard.rule.engine.api.TbNodeException: Unexpected originator EntityType: " + entityType; |
||||
|
|
||||
|
var exception = assertThrows(ExecutionException.class, |
||||
|
() -> EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, entityId).get()); |
||||
|
|
||||
|
assertInstanceOf(TbNodeException.class, exception.getCause()); |
||||
|
assertEquals(expectedExceptionMsg, exception.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue