diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java index 81bb3d6772..defc3fed0a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java @@ -49,11 +49,10 @@ import java.util.concurrent.TimeUnit; configClazz = TbMsgDeduplicationNodeConfiguration.class, nodeDescription = "Deduplicate messages within the same originator entity for a configurable period " + "based on a specified deduplication strategy.", - nodeDetails = "Rule node allows you to select one of the following strategy to deduplicate messages:

" + - "FIRST - return first message that arrived during deduplication period.

" + - "LAST - return last message that arrived during deduplication period.

" + - "ALL - return all messages as a single JSON array message. " + - "Where each element represents object with msg and metadata inner properties.

", + nodeDetails = "Deduplication strategy: ", icon = "content_copy", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeMsgDeduplicationConfig" diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index a52641bedc..f16d5bc666 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -15,8 +15,8 @@ */ package org.thingsboard.rule.engine.mail; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -30,9 +30,9 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -42,8 +42,11 @@ import java.util.Map; name = "to email", configClazz = TbMsgToEmailNodeConfiguration.class, nodeDescription = "Transforms message to email message", - nodeDetails = "Transforms message to email message by populating email fields using values derived from message metadata. " + - "Set 'SEND_EMAIL' output message type.", + nodeDetails = "Transforms message to email message. All email fields support templatization. " + + "Supports adding inline base64 images to the mail body from message metadata by checking for the images metadata key " + + "that should represent a map of entries: contentId to based64 image value.

" + + "Set SEND_EMAIL output message type if transformation completed successfully.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbTransformationNodeToEmailConfig", icon = "email" @@ -54,12 +57,12 @@ public class TbMsgToEmailNode implements TbNode { private static final String DYNAMIC = "dynamic"; private TbMsgToEmailNodeConfiguration config; - private boolean isDynamicHtmlTemplate; + private boolean dynamicMailBodyType; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class); - this.isDynamicHtmlTemplate = DYNAMIC.equals(this.config.getMailBodyType()); + this.dynamicMailBodyType = DYNAMIC.equals(this.config.getMailBodyType()); } @Override @@ -74,24 +77,22 @@ public class TbMsgToEmailNode implements TbNode { } } - private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) throws JsonProcessingException { + private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) { String emailJson = JacksonUtil.toString(email); return ctx.transformMsg(msg, TbMsgType.SEND_EMAIL, msg.getOriginator(), msg.getMetaData().copy(), emailJson); } - private TbEmail convert(TbMsg msg) throws IOException { + private TbEmail convert(TbMsg msg) { TbEmail.TbEmailBuilder builder = TbEmail.builder(); - builder.from(fromTemplate(this.config.getFromTemplate(), msg)); - builder.to(fromTemplate(this.config.getToTemplate(), msg)); - builder.cc(fromTemplate(this.config.getCcTemplate(), msg)); - builder.bcc(fromTemplate(this.config.getBccTemplate(), msg)); - if(isDynamicHtmlTemplate) { - builder.html(Boolean.parseBoolean(fromTemplate(this.config.getIsHtmlTemplate(), msg))); - } else { - builder.html(Boolean.parseBoolean(this.config.getMailBodyType())); - } - builder.subject(fromTemplate(this.config.getSubjectTemplate(), msg)); - builder.body(fromTemplate(this.config.getBodyTemplate(), msg)); + builder.from(fromTemplate(config.getFromTemplate(), msg)); + builder.to(fromTemplate(config.getToTemplate(), msg)); + builder.cc(fromTemplate(config.getCcTemplate(), msg)); + builder.bcc(fromTemplate(config.getBccTemplate(), msg)); + String htmlStr = dynamicMailBodyType ? + fromTemplate(config.getIsHtmlTemplate(), msg) : config.getMailBodyType(); + builder.html(Boolean.parseBoolean(htmlStr)); + builder.subject(fromTemplate(config.getSubjectTemplate(), msg)); + builder.body(fromTemplate(config.getBodyTemplate(), msg)); String imagesStr = msg.getMetaData().getValue(IMAGES); if (!StringUtils.isEmpty(imagesStr)) { Map imgMap = JacksonUtil.fromString(imagesStr, new TypeReference>() {}); @@ -101,11 +102,7 @@ public class TbMsgToEmailNode implements TbNode { } private String fromTemplate(String template, TbMsg msg) { - if (!StringUtils.isEmpty(template)) { - return TbNodeUtils.processPattern(template, msg); - } else { - return null; - } + return StringUtils.isNotEmpty(template) ? TbNodeUtils.processPattern(template, msg) : null; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java index 7b8ddefa7f..f31b5a3893 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java @@ -19,7 +19,7 @@ import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; @Data -public class TbMsgToEmailNodeConfiguration implements NodeConfiguration { +public class TbMsgToEmailNodeConfiguration implements NodeConfiguration { private String fromTemplate; private String toTemplate; @@ -28,16 +28,16 @@ public class TbMsgToEmailNodeConfiguration implements NodeConfiguration { private String subjectTemplate; private String bodyTemplate; private String isHtmlTemplate; - private String mailBodyType; + private String mailBodyType; // Plain Text -> false. HTML - true. Dynamic - value used from isHtmlTemplate. @Override public TbMsgToEmailNodeConfiguration defaultConfiguration() { - TbMsgToEmailNodeConfiguration configuration = new TbMsgToEmailNodeConfiguration(); - configuration.fromTemplate = "info@testmail.org"; - configuration.toTemplate = "${userEmail}"; - configuration.subjectTemplate = "Device ${deviceType} temperature high"; - configuration.bodyTemplate = "Device ${deviceName} has high temperature ${temp}"; - configuration.mailBodyType = "false"; + var configuration = new TbMsgToEmailNodeConfiguration(); + configuration.setFromTemplate("info@testmail.org"); + configuration.setToTemplate("${userEmail}"); + configuration.setSubjectTemplate("Device ${deviceType} temperature high"); + configuration.setBodyTemplate("Device ${deviceName} has high temperature $[temperature]"); + configuration.setMailBodyType("false"); return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index 9efc051e2b..a26cec57ee 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -43,13 +43,15 @@ import java.util.NoSuchElementException; type = ComponentType.TRANSFORMATION, name = "change originator", configClazz = TbChangeOriginatorNodeConfiguration.class, - nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity/Alarm Originator/Entity by name pattern", - nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + - "If multiple related entities are found, only first entity is used as new originator, other entities are discarded.
" + - "Alarm Originator might be found only if the original Originator is Alarm entity.
" + - "Entity by name pattern lookup only if found only in case original Originator is Alarm entity.
" + - "Lookup of an entity by name pattern requires selection of entity type and name pattern to be specified in the configuration. " + - "Allowed entity types to select: 'DEVICE', 'ASSET', 'ENTITY_VIEW', 'EDGE' and 'USER'.

" + + nodeDescription = "Change message originator to Tenant/Customer/Related Entity/Alarm Originator/Entity by name pattern.", + nodeDetails = "Configuration:
  • Tenant - use current tenant as new originator.
  • " + + "
  • Customer - use customer of incoming message originator as new originator. " + + "Only for originators with one of the following type: 'User', 'Asset', 'Device' that assigned to customers.
  • " + + "
  • Related Entity - use related entity as new originator. Lookup based on configured relation direction and relation type. " + + "If multiple related entities are found, only first entity is used as new originator, other entities are discarded.
  • " + + "
  • Alarm Originator - use alarm originator as new originator. Only if incoming message originator is alarm entity.
  • " + + "
  • Entity by name pattern - new originator lookup requires selection one of the following entity types: " + + "'Device', 'Asset', 'Entity View', 'Edge' or 'User' and input of name pattern.
" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbTransformationNodeChangeOriginatorConfig", diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java index 93b5ecb5d7..25467caf5a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java @@ -44,7 +44,8 @@ import java.util.stream.Collectors; configClazz = TbCopyKeysNodeConfiguration.class, nodeDescription = "Copies key-values from message to message metadata or vice-versa.", nodeDetails = "Fetches key-values from message or message metadata based on the keys list specified in the configuration " + - "and copies them into message metadata or message. Keys that are absent in the fetch source will be ignored. " + + "and copies them into message metadata or into message itself in accordance with the fetch source. " + + "Keys that are absent in the fetch source will be ignored. " + "Use regular expression(s) as a key(s) to copy keys by pattern.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java index 4666016f78..15ed71b0ae 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java @@ -43,7 +43,7 @@ import java.util.stream.Collectors; name = "delete key-values", version = 1, configClazz = TbDeleteKeysNodeConfiguration.class, - nodeDescription = "Removes key-values from message or metadata.", + nodeDescription = "Removes key-values from message or message metadata.", nodeDetails = "Removes key-values from message or message metadata based on the keys list specified in the configuration. " + "Use regular expression(s) as a key(s) to remove keys by pattern.

" + "Output connections: Success, Failure.", diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java index 9454bd51c4..6e00b98e3e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java @@ -38,9 +38,8 @@ import java.util.concurrent.ExecutionException; name = "json path", configClazz = TbJsonPathNodeConfiguration.class, nodeDescription = "Transforms incoming message body using JSONPath expression.", - nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure.
" + - "'$' represents the root object or array.
" + - "If JSONPath expression evaluation failed, incoming message routes via Failure chain.

" + + nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure. " + + "$ represents the root object or array.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, icon = "functions", diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java index d6e4e0dc34..7305c3ee78 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java @@ -39,9 +39,9 @@ import java.util.concurrent.ExecutionException; name = "rename keys", version = 1, configClazz = TbRenameKeysNodeConfiguration.class, - nodeDescription = "Renames message or message metadata key names to the new key names selected in the key mapping.", - nodeDetails = "If the key name that is selected in the key mapping is missed in the " + - "selected source(message or message metadata), it will be ignored.

" + + nodeDescription = "Renames message or message metadata key names.", + nodeDetails = "Renames key names in the message or message metadata based on the provided key names mapping. " + + "If key to rename doesn't exists in the specified source(message or message metadata) it will be ignored.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbTransformationNodeRenameKeysConfig", diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java index 7a3e53c69c..7a9c812ebc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java @@ -40,9 +40,8 @@ import java.util.concurrent.ExecutionException; name = "split array msg", configClazz = EmptyNodeConfiguration.class, nodeDescription = "Split array message into several messages", - nodeDetails = "Split the array fetched from the message. If the msg data is not a JSON array returns the " - + "incoming message as outbound message with Failure chain, otherwise returns " - + "inner objects of the extracted array as separate messages via Success chain.

" + + nodeDetails = "Split array message and returns inner objects as separate messages. " + + "Message metadata and type for each output message will be taken from original message.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, icon = "content_copy", diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java index 0cff412d39..79549edbd7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java @@ -34,9 +34,9 @@ import java.util.List; configClazz = TbTransformMsgNodeConfiguration.class, nodeDescription = "Change Message payload, Metadata or Message type using JavaScript", nodeDetails = "JavaScript function receive 3 input parameters
" + - "metadata - is a Message metadata.
" + - "msg - is a Message payload.
" + - "msgType - is a Message type.
" + + "msg - is a message payload.
" + + "metadata - is a message metadata.
" + + "msgType - is a message type.
" + "Should return the following structure:
" + "{ msg: new payload,
   metadata: new metadata,
   msgType: new msgType }

" + "All fields in resulting object are optional and will be taken from original message if not specified.

" + diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java index 9579c73058..5a19fa860f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java @@ -44,8 +44,7 @@ public class EntitiesCustomerIdAsyncLoader { } private static ListenableFuture toCustomerIdAsync(TbContext ctx, ListenableFuture future) { - return Futures.transformAsync(future, in -> in != null ? Futures.immediateFuture(in.getCustomerId()) - : Futures.immediateFuture(null), ctx.getDbCallbackExecutor()); + return Futures.transform(future, in -> in != null ? in.getCustomerId() : null, ctx.getDbCallbackExecutor()); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java index bf4d090c1c..4f1b0488d7 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java @@ -15,12 +15,15 @@ */ package org.thingsboard.rule.engine.mail; -import com.datastax.oss.driver.api.core.uuid.Uuids; -import org.junit.Test; -import org.junit.runner.RunWith; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; 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.TbEmail; @@ -28,85 +31,135 @@ 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.EntityId; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import java.io.IOException; +import java.util.UUID; +import java.util.stream.Stream; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotSame; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -@RunWith(MockitoJUnitRunner.class) public class TbMsgToEmailNodeTest { - private TbMsgToEmailNode emailNode; + private static final int EXPECTED_TEMPERATURE = 30; + private static final String EXPECTED_DEVICE_NAME = "TH-001"; + private static final String EXPECTED_DEVICE_TYPE = "thermostat"; + private static final String EXPECTED_SUBJECT = "Device " + EXPECTED_DEVICE_TYPE + " temperature high"; + private static final String EXPECTED_BODY = "Device " + EXPECTED_DEVICE_NAME + " has high temperature " + EXPECTED_TEMPERATURE; + private static final String EXPECTED_TO_EMAIL = "user@email.io"; + private static final String DYNAMIC_MAIL_BODY_TYPE = "dynamic"; + + private EntityId originator; + private TbMsgToEmailNode node; + private TbMsgToEmailNodeConfiguration config; + + private TbContext ctxMock; + + @BeforeEach + void setUp() throws TbNodeException { + ctxMock = mock(TbContext.class); + originator = new DeviceId(UUID.randomUUID()); + config = new TbMsgToEmailNodeConfiguration().defaultConfiguration(); + node = new TbMsgToEmailNode(); + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } - @Mock - private TbContext ctx; + @AfterEach + void tearDown() { + node.destroy(); + } - private EntityId originator = new DeviceId(Uuids.timeBased()); - private TbMsgMetaData metaData = new TbMsgMetaData(); - private String rawJson = "{\"name\": \"temp\", \"passed\": 5 , \"complex\": {\"val\":12, \"count\":100}}"; + @Test + void givenDefaultConfig_whenVerify_thenOK() { + // GIVEN-WHEN-THEN + assertThat(config.getFromTemplate()).isEqualTo("info@testmail.org"); + assertThat(config.getToTemplate()).isEqualTo("${userEmail}"); + assertThat(config.getSubjectTemplate()).isEqualTo("Device ${deviceType} temperature high"); + assertThat(config.getBodyTemplate()).isEqualTo("Device ${deviceName} has high temperature $[temperature]"); + } - private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + static Stream MailBodyTypeTestConfig() { + return Stream.of( + new MailBodyTypeTestConfig(false, "false", null), + new MailBodyTypeTestConfig(false, null, null), + new MailBodyTypeTestConfig(false, DYNAMIC_MAIL_BODY_TYPE, "false"), + new MailBodyTypeTestConfig(true, DYNAMIC_MAIL_BODY_TYPE, "true"), + new MailBodyTypeTestConfig(true, "true", null) + ); + } - @Test - public void msgCanBeConverted() { - initWithScript(); - metaData.putValue("username", "oreo"); - metaData.putValue("userEmail", "user@email.io"); - metaData.putValue("name", "temp"); - metaData.putValue("passed", "5"); - metaData.putValue("count", "100"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - - emailNode.onMsg(ctx, msg); - - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - - assertEquals(TbMsgType.SEND_EMAIL, typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); - assertEquals("oreo", metadataCaptor.getValue().getValue("username")); - assertNotSame(metaData, metadataCaptor.getValue()); - - TbEmail actual = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), TbEmail.class); - - TbEmail expected = TbEmail.builder() - .from("test@mail.org") - .to("user@email.io") - .subject("Hi oreo there") - .body("temp is to high. Current 5 and 100") + @ParameterizedTest + @MethodSource("MailBodyTypeTestConfig") + public void givenMailBodyTypeTestConfig_whenOnMsg_thenVerify(MailBodyTypeTestConfig testConfig) throws TbNodeException { + // GIVEN + String mailBodyType = testConfig.getMailBodyType(); + config.setMailBodyType(mailBodyType); + if (DYNAMIC_MAIL_BODY_TYPE.equals(mailBodyType)) { + config.setIsHtmlTemplate("${html}"); + } + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var md = new TbMsgMetaData(); + md.putValue("userEmail", EXPECTED_TO_EMAIL); + md.putValue("deviceType", EXPECTED_DEVICE_TYPE); + md.putValue("deviceName", EXPECTED_DEVICE_NAME); + if (testConfig.getMdValue() != null) { + md.putValue("html", testConfig.getMdValue()); + } + + var msgDataStr = "{\"temperature\": " + EXPECTED_TEMPERATURE + "}"; + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, md, msgDataStr); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + var typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); + var originatorCaptor = ArgumentCaptor.forClass(EntityId.class); + var metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + var dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctxMock, times(1)).transformMsg( + msgCaptor.capture(), + typeCaptor.capture(), + originatorCaptor.capture(), + metadataCaptor.capture(), + dataCaptor.capture() + ); + verify(ctxMock, never()).tellFailure(any(), any()); + + Assertions.assertEquals(TbMsgType.SEND_EMAIL, typeCaptor.getValue()); + Assertions.assertEquals(originator, originatorCaptor.getValue()); + Assertions.assertNotSame(md, metadataCaptor.getValue()); + + var actual = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), TbEmail.class); + var expected = getExpectedTbEmail(testConfig.isExpectedHtmlValue()); + + Assertions.assertEquals(expected, actual); + } + + private TbEmail getExpectedTbEmail(boolean html) { + return TbEmail.builder() + .from(config.getFromTemplate()) + .to(EXPECTED_TO_EMAIL) + .subject(EXPECTED_SUBJECT) + .body(EXPECTED_BODY) + .html(html) .build(); - assertEquals(expected, actual); } - private void initWithScript() { - try { - TbMsgToEmailNodeConfiguration config = new TbMsgToEmailNodeConfiguration(); - config.setFromTemplate("test@mail.org"); - config.setToTemplate("${userEmail}"); - config.setSubjectTemplate("Hi ${username} there"); - config.setBodyTemplate("${name} is to high. Current ${passed} and ${count}"); - config.setMailBodyType("false"); - TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - - emailNode = new TbMsgToEmailNode(); - emailNode.init(ctx, nodeConfiguration); - } catch (TbNodeException ex) { - throw new IllegalStateException(ex); - } + @Data + @RequiredArgsConstructor + static class MailBodyTypeTestConfig { + private final boolean expectedHtmlValue; + private final String mailBodyType; + private final String mdValue; } }