AI rule node: fix flaky Azure IoT hub node test

This commit is contained in:
Dmytro Skarzhynets 2025-07-10 11:18:59 +03:00
parent 9bcd3d8849
commit fd7faa5a9e
No known key found for this signature in database
GPG Key ID: 2B51652F224037DF
3 changed files with 30 additions and 16 deletions

View File

@ -26,11 +26,13 @@ import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.Clock;
import java.util.Base64; import java.util.Base64;
import java.util.Iterator; import java.util.Iterator;
@Slf4j @Slf4j
public final class AzureIotHubUtil { public final class AzureIotHubUtil {
private static final String BASE_DIR_PATH = System.getProperty("user.dir"); private static final String BASE_DIR_PATH = System.getProperty("user.dir");
private static final String APP_DIR = "application"; private static final String APP_DIR = "application";
private static final String SRC_DIR = "src"; 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 SAS_TOKEN_VALID_SECS = 365 * 24 * 60 * 60; // one year
private static final long ONE_SECOND_IN_MILLISECONDS = 1000;
private static final String SAS_TOKEN_FORMAT = "SharedAccessSignature sr=%s&sig=%s&se=%s"; 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 static final String USERNAME_FORMAT = "%s/%s/?api-version=2018-06-30";
private AzureIotHubUtil() { private AzureIotHubUtil() {}
}
public static String buildUsername(String host, String deviceId) { public static String buildUsername(String host, String deviceId) {
return String.format(USERNAME_FORMAT, host, 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 { try {
final String targetUri = URLEncoder.encode(host.toLowerCase(), "UTF-8"); final String targetUri = URLEncoder.encode(host.toLowerCase(), StandardCharsets.UTF_8);
final long expiryTime = buildExpiresOn(); final long expiryTime = buildExpiresOn(clock);
String toSign = targetUri + "\n" + expiryTime; String toSign = targetUri + "\n" + expiryTime;
byte[] keyBytes = Base64.getDecoder().decode(sasKey.getBytes(StandardCharsets.UTF_8)); byte[] keyBytes = Base64.getDecoder().decode(sasKey.getBytes(StandardCharsets.UTF_8));
SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA256"); SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256");
mac.init(signingKey); mac.init(signingKey);
byte[] rawHmac = mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8)); 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); return String.format(SAS_TOKEN_FORMAT, targetUri, signature, expiryTime);
} catch (Exception e) { } 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() { private static long buildExpiresOn(Clock clock) {
long expiresOnDate = System.currentTimeMillis(); return clock.instant().plusSeconds(SAS_TOKEN_VALID_SECS).getEpochSecond();
expiresOnDate += SAS_TOKEN_VALID_SECS * ONE_SECOND_IN_MILLISECONDS;
return expiresOnDate / ONE_SECOND_IN_MILLISECONDS;
} }
public static String getDefaultCaCert() { public static String getDefaultCaCert() {

View File

@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.mqtt.azure;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.codec.mqtt.MqttVersion;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.AzureIotHubUtil; 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.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.data.util.TbPair;
import java.time.Clock;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.EXTERNAL, type = ComponentType.EXTERNAL,
@ -49,6 +52,8 @@ import org.thingsboard.server.common.data.util.TbPair;
) )
public class TbAzureIotHubNode extends TbMqttNode { public class TbAzureIotHubNode extends TbMqttNode {
private Clock clock = Clock.systemUTC();
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx); super.init(ctx);
@ -73,7 +78,7 @@ public class TbAzureIotHubNode extends TbMqttNode {
config.setUsername(AzureIotHubUtil.buildUsername(mqttNodeConfiguration.getHost(), config.getClientId())); config.setUsername(AzureIotHubUtil.buildUsername(mqttNodeConfiguration.getHost(), config.getClientId()));
ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); ClientCredentials credentials = mqttNodeConfiguration.getCredentials();
if (CredentialsType.SAS == credentials.getType()) { 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); return initClient(ctx);
} }
@VisibleForTesting
void setClock(Clock clock) {
this.clock = clock;
}
@Override @Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false; boolean hasChanges = false;

View File

@ -34,6 +34,9 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.credentials.CertPemCredentials; import org.thingsboard.rule.engine.credentials.CertPemCredentials;
import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration; 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 java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -77,7 +80,10 @@ public class TbAzureIotHubNodeTest extends AbstractRuleNodeUpgradeTest {
@Test @Test
public void verifyPrepareMqttClientConfigMethodWithAzureIotHubSasCredentials() throws Exception { 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.setSasKey("testSasKey");
credentials.setCaCert("test-ca-cert.pem"); credentials.setCaCert("test-ca-cert.pem");
azureIotHubNodeConfig.setCredentials(credentials); azureIotHubNodeConfig.setCredentials(credentials);
@ -89,7 +95,7 @@ public class TbAzureIotHubNodeTest extends AbstractRuleNodeUpgradeTest {
azureIotHubNode.prepareMqttClientConfig(mqttClientConfig); azureIotHubNode.prepareMqttClientConfig(mqttClientConfig);
assertThat(mqttClientConfig.getUsername()).isEqualTo(AzureIotHubUtil.buildUsername(azureIotHubNodeConfig.getHost(), mqttClientConfig.getClientId())); 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 @Test