From fd7faa5a9e06f1a2a89dabcc7bfac4efd2303cf3 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 10 Jul 2025 11:18:59 +0300 Subject: [PATCH] AI rule node: fix flaky Azure IoT hub node test --- .../common/util/AzureIotHubUtil.java | 24 +++++++++---------- .../engine/mqtt/azure/TbAzureIotHubNode.java | 12 +++++++++- .../mqtt/azure/TbAzureIotHubNodeTest.java | 10 ++++++-- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java b/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java index 2c214460f6..001513b008 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java @@ -26,11 +26,13 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Clock; import java.util.Base64; import java.util.Iterator; @Slf4j public final class AzureIotHubUtil { + private static final String BASE_DIR_PATH = System.getProperty("user.dir"); private static final String APP_DIR = "application"; private static final String SRC_DIR = "src"; @@ -52,41 +54,37 @@ public final class AzureIotHubUtil { } } - private static final long SAS_TOKEN_VALID_SECS = 365 * 24 * 60 * 60; - private static final long ONE_SECOND_IN_MILLISECONDS = 1000; + private static final long SAS_TOKEN_VALID_SECS = 365 * 24 * 60 * 60; // one year private static final String SAS_TOKEN_FORMAT = "SharedAccessSignature sr=%s&sig=%s&se=%s"; private static final String USERNAME_FORMAT = "%s/%s/?api-version=2018-06-30"; - private AzureIotHubUtil() { - } + private AzureIotHubUtil() {} public static String buildUsername(String host, String deviceId) { return String.format(USERNAME_FORMAT, host, deviceId); } - public static String buildSasToken(String host, String sasKey) { + public static String buildSasToken(String host, String sasKey, Clock clock) { try { - final String targetUri = URLEncoder.encode(host.toLowerCase(), "UTF-8"); - final long expiryTime = buildExpiresOn(); + final String targetUri = URLEncoder.encode(host.toLowerCase(), StandardCharsets.UTF_8); + final long expiryTime = buildExpiresOn(clock); String toSign = targetUri + "\n" + expiryTime; byte[] keyBytes = Base64.getDecoder().decode(sasKey.getBytes(StandardCharsets.UTF_8)); SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(signingKey); byte[] rawHmac = mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8)); - String signature = URLEncoder.encode(Base64.getEncoder().encodeToString(rawHmac), "UTF-8"); + String signature = URLEncoder.encode(Base64.getEncoder().encodeToString(rawHmac), StandardCharsets.UTF_8); return String.format(SAS_TOKEN_FORMAT, targetUri, signature, expiryTime); } catch (Exception e) { - throw new RuntimeException("Failed to build SAS token!!!", e); + throw new RuntimeException("Failed to build SAS token!", e); } } - private static long buildExpiresOn() { - long expiresOnDate = System.currentTimeMillis(); - expiresOnDate += SAS_TOKEN_VALID_SECS * ONE_SECOND_IN_MILLISECONDS; - return expiresOnDate / ONE_SECOND_IN_MILLISECONDS; + private static long buildExpiresOn(Clock clock) { + return clock.instant().plusSeconds(SAS_TOKEN_VALID_SECS).getEpochSecond(); } public static String getDefaultCaCert() { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java index 2ea56ce799..26c5b3fa42 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.mqtt.azure; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.annotations.VisibleForTesting; import io.netty.handler.codec.mqtt.MqttVersion; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.AzureIotHubUtil; @@ -36,6 +37,8 @@ import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; +import java.time.Clock; + @Slf4j @RuleNode( type = ComponentType.EXTERNAL, @@ -49,6 +52,8 @@ import org.thingsboard.server.common.data.util.TbPair; ) public class TbAzureIotHubNode extends TbMqttNode { + private Clock clock = Clock.systemUTC(); + @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx); @@ -73,7 +78,7 @@ public class TbAzureIotHubNode extends TbMqttNode { config.setUsername(AzureIotHubUtil.buildUsername(mqttNodeConfiguration.getHost(), config.getClientId())); ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); if (CredentialsType.SAS == credentials.getType()) { - config.setPassword(AzureIotHubUtil.buildSasToken(mqttNodeConfiguration.getHost(), ((AzureIotHubSasCredentials) credentials).getSasKey())); + config.setPassword(AzureIotHubUtil.buildSasToken(mqttNodeConfiguration.getHost(), ((AzureIotHubSasCredentials) credentials).getSasKey(), clock)); } } @@ -81,6 +86,11 @@ public class TbAzureIotHubNode extends TbMqttNode { return initClient(ctx); } + @VisibleForTesting + void setClock(Clock clock) { + this.clock = clock; + } + @Override public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { boolean hasChanges = false; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java index 433d5d4673..c8c1553fa5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java @@ -34,6 +34,9 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.credentials.CertPemCredentials; import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -77,7 +80,10 @@ public class TbAzureIotHubNodeTest extends AbstractRuleNodeUpgradeTest { @Test public void verifyPrepareMqttClientConfigMethodWithAzureIotHubSasCredentials() throws Exception { - AzureIotHubSasCredentials credentials = new AzureIotHubSasCredentials(); + var fixedClock = Clock.fixed(Instant.parse("2030-01-01T00:00:00Z"), ZoneOffset.UTC); + azureIotHubNode.setClock(fixedClock); + + var credentials = new AzureIotHubSasCredentials(); credentials.setSasKey("testSasKey"); credentials.setCaCert("test-ca-cert.pem"); azureIotHubNodeConfig.setCredentials(credentials); @@ -89,7 +95,7 @@ public class TbAzureIotHubNodeTest extends AbstractRuleNodeUpgradeTest { azureIotHubNode.prepareMqttClientConfig(mqttClientConfig); assertThat(mqttClientConfig.getUsername()).isEqualTo(AzureIotHubUtil.buildUsername(azureIotHubNodeConfig.getHost(), mqttClientConfig.getClientId())); - assertThat(mqttClientConfig.getPassword()).isEqualTo(AzureIotHubUtil.buildSasToken(azureIotHubNodeConfig.getHost(), credentials.getSasKey())); + assertThat(mqttClientConfig.getPassword()).isEqualTo(AzureIotHubUtil.buildSasToken(azureIotHubNodeConfig.getHost(), credentials.getSasKey(), fixedClock)); } @Test