|
|
@ -31,7 +31,9 @@ import org.junit.Assert; |
|
|
import org.thingsboard.server.common.data.Device; |
|
|
import org.thingsboard.server.common.data.Device; |
|
|
import org.thingsboard.server.common.data.StringUtils; |
|
|
import org.thingsboard.server.common.data.StringUtils; |
|
|
import org.thingsboard.server.common.data.TransportPayloadType; |
|
|
import org.thingsboard.server.common.data.TransportPayloadType; |
|
|
|
|
|
import org.thingsboard.server.common.data.asset.AssetInfo; |
|
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|
|
|
|
|
import org.thingsboard.server.common.data.id.DeviceId; |
|
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|
|
import org.thingsboard.server.common.data.kv.BooleanDataEntry; |
|
|
import org.thingsboard.server.common.data.kv.BooleanDataEntry; |
|
|
import org.thingsboard.server.common.data.kv.DoubleDataEntry; |
|
|
import org.thingsboard.server.common.data.kv.DoubleDataEntry; |
|
|
@ -39,6 +41,7 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry; |
|
|
import org.thingsboard.server.common.data.kv.LongDataEntry; |
|
|
import org.thingsboard.server.common.data.kv.LongDataEntry; |
|
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
|
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
|
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|
|
|
|
|
import org.thingsboard.server.common.data.relation.EntityRelation; |
|
|
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; |
|
|
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; |
|
|
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; |
|
|
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; |
|
|
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; |
|
|
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; |
|
|
@ -57,6 +60,7 @@ import java.util.concurrent.atomic.AtomicReference; |
|
|
|
|
|
|
|
|
import static org.awaitility.Awaitility.await; |
|
|
import static org.awaitility.Awaitility.await; |
|
|
import static org.eclipse.paho.mqttv5.common.packet.MqttWireMessage.MESSAGE_TYPE_CONNACK; |
|
|
import static org.eclipse.paho.mqttv5.common.packet.MqttWireMessage.MESSAGE_TYPE_CONNACK; |
|
|
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|
|
import static org.thingsboard.common.util.JacksonUtil.newArrayNode; |
|
|
import static org.thingsboard.common.util.JacksonUtil.newArrayNode; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Bytes; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Bytes; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int16; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int16; |
|
|
@ -67,8 +71,13 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataTyp |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt64; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt64; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt8; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt8; |
|
|
|
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; |
|
|
|
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; |
|
|
|
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; |
|
|
|
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.DEVICE_NAME_SPLIT_SEPARATOR; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_ROOT_SPB_V_1_0; |
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_ROOT_SPB_V_1_0; |
|
|
|
|
|
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_SEPARATOR; |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Created by nickAS21 on 12.01.23 |
|
|
* Created by nickAS21 on 12.01.23 |
|
|
@ -82,6 +91,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte |
|
|
protected ThreadLocalRandom random = ThreadLocalRandom.current(); |
|
|
protected ThreadLocalRandom random = ThreadLocalRandom.current(); |
|
|
|
|
|
|
|
|
protected static final String groupId = "SparkplugBGroupId"; |
|
|
protected static final String groupId = "SparkplugBGroupId"; |
|
|
|
|
|
protected static final String edgeNodeDeviceName = "Test Connect Sparkplug client node"; |
|
|
protected static final String edgeNode = "SparkpluBNode"; |
|
|
protected static final String edgeNode = "SparkpluBNode"; |
|
|
protected static final String keysBdSeq = "bdSeq"; |
|
|
protected static final String keysBdSeq = "bdSeq"; |
|
|
protected static final String alias = "Failed Telemetry/Attribute proto sparkplug payload. SparkplugMessageType "; |
|
|
protected static final String alias = "Failed Telemetry/Attribute proto sparkplug payload. SparkplugMessageType "; |
|
|
@ -98,14 +108,32 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte |
|
|
protected static final String metricBirthName_Int32 = "Device Metric int32"; |
|
|
protected static final String metricBirthName_Int32 = "Device Metric int32"; |
|
|
protected Set<String> sparkplugAttributesMetricNames; |
|
|
protected Set<String> sparkplugAttributesMetricNames; |
|
|
|
|
|
|
|
|
public void beforeSparkplugTest() throws Exception { |
|
|
public void beforeSparkplugTest(boolean isCreateDevices) throws Exception { |
|
|
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() |
|
|
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() |
|
|
.gatewayName("Test Connect Sparkplug client node") |
|
|
.gatewayName(edgeNodeDeviceName) |
|
|
.isSparkplug(true) |
|
|
.isSparkplug(true) |
|
|
.sparkplugAttributesMetricNames(sparkplugAttributesMetricNames) |
|
|
.sparkplugAttributesMetricNames(sparkplugAttributesMetricNames) |
|
|
.transportPayloadType(TransportPayloadType.PROTOBUF) |
|
|
.transportPayloadType(TransportPayloadType.PROTOBUF) |
|
|
.build(); |
|
|
.build(); |
|
|
processBeforeTest(configProperties); |
|
|
processBeforeTest(configProperties); |
|
|
|
|
|
if (isCreateDevices) { |
|
|
|
|
|
// 1. Create the first device with a short name (legacy style)
|
|
|
|
|
|
String deviceName1 = deviceId + "_1"; |
|
|
|
|
|
Device device1 = createDevice(deviceName1, deviceProfile.getName(), false); |
|
|
|
|
|
|
|
|
|
|
|
// 2. Establish 'Created' relation so the transport identifies this gateway as the owner
|
|
|
|
|
|
String relationType = "Created"; |
|
|
|
|
|
EntityRelation relation1 = createFromRelation(savedGateway, device1, relationType); |
|
|
|
|
|
doPost("/api/relation", relation1).andExpect(status().isOk()); |
|
|
|
|
|
|
|
|
|
|
|
// 3. Create the second device with a full-path name
|
|
|
|
|
|
String deviceName2 = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + deviceId + "_2"; |
|
|
|
|
|
Device device2 = createDevice(deviceName2, deviceProfile.getName(), false); |
|
|
|
|
|
|
|
|
|
|
|
// 4. Establish 'Created' relation for the second device as well
|
|
|
|
|
|
EntityRelation relation2 = createFromRelation(savedGateway, device2, relationType); |
|
|
|
|
|
doPost("/api/relation", relation2).andExpect(status().isOk()); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public void clientWithCorrectNodeAccessTokenWithNDEATH() throws Exception { |
|
|
public void clientWithCorrectNodeAccessTokenWithNDEATH() throws Exception { |
|
|
@ -146,7 +174,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte |
|
|
options.setSessionExpiryInterval(0L); |
|
|
options.setSessionExpiryInterval(0L); |
|
|
options.setUserName(gatewayAccessToken); |
|
|
options.setUserName(gatewayAccessToken); |
|
|
String nameSpace = nameSpaceBad.length == 0 ? TOPIC_ROOT_SPB_V_1_0 : nameSpaceBad[0]; |
|
|
String nameSpace = nameSpaceBad.length == 0 ? TOPIC_ROOT_SPB_V_1_0 : nameSpaceBad[0]; |
|
|
String topic = nameSpace + "/" + groupId + "/" + SparkplugMessageType.NDEATH.name() + "/" + edgeNode; |
|
|
String topic = nameSpace + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.NDEATH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode; |
|
|
// The NDEATH message MUST set the MQTT Will QoS to 1 and Retained flag to false
|
|
|
// The NDEATH message MUST set the MQTT Will QoS to 1 and Retained flag to false
|
|
|
MqttMessage msg = new MqttMessage(); |
|
|
MqttMessage msg = new MqttMessage(); |
|
|
msg.setId(0); |
|
|
msg.setId(0); |
|
|
@ -159,38 +187,35 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte |
|
|
protected List<Device> connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices, long ts) throws Exception { |
|
|
protected List<Device> connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices, long ts) throws Exception { |
|
|
List<Device> devices = new ArrayList<>(); |
|
|
List<Device> devices = new ArrayList<>(); |
|
|
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|
|
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|
|
String keyInt = "Node Metric int32"; |
|
|
MetricDataType metricDataType = Int32; |
|
|
|
|
|
String key = "Node Metric int32"; |
|
|
int valueDeviceInt32 = 1024; |
|
|
int valueDeviceInt32 = 1024; |
|
|
SparkplugBProto.Payload.Metric metricInt = createMetric(valueDeviceInt32, ts, keyInt, Int32, -1L); |
|
|
SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType, -1L); |
|
|
String keyStringEmpty = "Node Metric String Empty"; |
|
|
|
|
|
String valueDeviceStringEmpty = ""; |
|
|
|
|
|
SparkplugBProto.Payload.Metric metricStringEmpty = createMetric(valueDeviceStringEmpty, ts, keyStringEmpty, MetricDataType.String, -1L); |
|
|
|
|
|
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() |
|
|
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() |
|
|
.setTimestamp(ts) |
|
|
.setTimestamp(ts) |
|
|
.setSeq(getBdSeqNum()); |
|
|
.setSeq(getBdSeqNum()); |
|
|
payloadBirthNode.addMetrics(metricInt); |
|
|
payloadBirthNode.addMetrics(metric); |
|
|
payloadBirthNode.addMetrics(metricStringEmpty); |
|
|
|
|
|
payloadBirthNode.setTimestamp(ts); |
|
|
payloadBirthNode.setTimestamp(ts); |
|
|
if (client.isConnected()) { |
|
|
if (client.isConnected()) { |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.NBIRTH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode, |
|
|
payloadBirthNode.build().toByteArray(), 0, false); |
|
|
payloadBirthNode.build().toByteArray(), 0, false); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
valueDeviceInt32 = 4024; |
|
|
valueDeviceInt32 = 4024; |
|
|
metricInt = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32, -1L); |
|
|
metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32, -1L); |
|
|
for (int i = 0; i < cntDevices; i++) { |
|
|
for (int i = 0; i < cntDevices; i++) { |
|
|
SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() |
|
|
SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() |
|
|
.setTimestamp(ts) |
|
|
.setTimestamp(ts) |
|
|
.setSeq(getSeqNum()); |
|
|
.setSeq(getSeqNum()); |
|
|
String deviceName = deviceId + "_" + i; |
|
|
String deviceIdName = deviceId + "_" + i; |
|
|
|
|
|
String deviceName = groupId + ":" + edgeNode + ":" + deviceIdName; |
|
|
payloadBirthDevice.addMetrics(metricInt); |
|
|
payloadBirthDevice.addMetrics(metric); |
|
|
if (client.isConnected()) { |
|
|
if (client.isConnected()) { |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName, |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.DBIRTH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode + TOPIC_SPLIT_SEPARATOR + deviceIdName, |
|
|
payloadBirthDevice.build().toByteArray(), 0, false); |
|
|
payloadBirthDevice.build().toByteArray(), 0, false); |
|
|
AtomicReference<Device> device = new AtomicReference<>(); |
|
|
AtomicReference<Device> device = new AtomicReference<>(); |
|
|
await(alias + "find device [" + deviceName + "] after created") |
|
|
await(alias + "find device [" + deviceIdName + "] after created") |
|
|
.atMost(200, TimeUnit.SECONDS) |
|
|
.atMost(40, TimeUnit.SECONDS) |
|
|
.ignoreExceptions() |
|
|
.ignoreExceptions() |
|
|
.until(() -> { |
|
|
.until(() -> { |
|
|
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); |
|
|
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); |
|
|
@ -198,13 +223,265 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte |
|
|
}); |
|
|
}); |
|
|
devices.add(device.get()); |
|
|
devices.add(device.get()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
Assert.assertEquals(cntDevices, devices.size()); |
|
|
Assert.assertEquals(cntDevices, devices.size()); |
|
|
return devices; |
|
|
return devices; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
protected void connectClientWithCorrectAccessTokenWithNDEATHDevicesCreatingBefore_Test(int cntDevices) throws Exception { |
|
|
|
|
|
long ts = calendar.getTimeInMillis(); |
|
|
|
|
|
List<Device> devices = new ArrayList<>(); |
|
|
|
|
|
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|
|
|
|
|
MetricDataType metricDataType = Int32; |
|
|
|
|
|
String key = "Node Metric int32"; |
|
|
|
|
|
int valueDeviceInt32 = 1024; |
|
|
|
|
|
SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType, -1L); |
|
|
|
|
|
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() |
|
|
|
|
|
.setTimestamp(ts) |
|
|
|
|
|
.setSeq(getBdSeqNum()); |
|
|
|
|
|
payloadBirthNode.addMetrics(metric); |
|
|
|
|
|
payloadBirthNode.setTimestamp(ts); |
|
|
|
|
|
if (client.isConnected()) { |
|
|
|
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.NBIRTH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode, |
|
|
|
|
|
payloadBirthNode.build().toByteArray(), 0, false); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
valueDeviceInt32 = 4024; |
|
|
|
|
|
metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32, -1L); |
|
|
|
|
|
// as old device name -> deviceId
|
|
|
|
|
|
String deviceIdNameLabel1 = deviceId + "_1"; |
|
|
|
|
|
|
|
|
|
|
|
if (client.isConnected()) { |
|
|
|
|
|
SparkplugBProto.Payload.Builder payloadBirthDevice1 = SparkplugBProto.Payload.newBuilder() |
|
|
|
|
|
.setTimestamp(ts) |
|
|
|
|
|
.setSeq(getSeqNum()); |
|
|
|
|
|
payloadBirthDevice1.addMetrics(metric); |
|
|
|
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.DBIRTH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode + TOPIC_SPLIT_SEPARATOR + deviceIdNameLabel1, |
|
|
|
|
|
payloadBirthDevice1.build().toByteArray(), 0, false); |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
String deviceName1 = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + deviceIdNameLabel1; |
|
|
|
|
|
AtomicReference<Device> device1 = new AtomicReference<>(); |
|
|
|
|
|
await(alias + "find device [" + deviceName1 + "] before connecting") |
|
|
|
|
|
.atMost(40, TimeUnit.SECONDS) |
|
|
|
|
|
.until(() -> { |
|
|
|
|
|
device1.set(doGet("/api/tenant/devices?deviceName=" + deviceName1, Device.class)); |
|
|
|
|
|
return device1.get() != null; |
|
|
|
|
|
}); |
|
|
|
|
|
devices.add(device1.get()); |
|
|
|
|
|
|
|
|
|
|
|
// as new device name -> groupId + ":" + edgeNode + ":" + deviceId;
|
|
|
|
|
|
String deviceIdName2 = deviceId + "_2"; |
|
|
|
|
|
if (client.isConnected()) { |
|
|
|
|
|
SparkplugBProto.Payload.Builder payloadBirthDevice2 = SparkplugBProto.Payload.newBuilder() |
|
|
|
|
|
.setTimestamp(ts) |
|
|
|
|
|
.setSeq(getSeqNum()); |
|
|
|
|
|
payloadBirthDevice2.addMetrics(metric); |
|
|
|
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.DBIRTH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode + TOPIC_SPLIT_SEPARATOR + deviceIdName2, |
|
|
|
|
|
payloadBirthDevice2.build().toByteArray(), 0, false); |
|
|
|
|
|
} |
|
|
|
|
|
String deviceName2 = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + deviceIdName2; |
|
|
|
|
|
AtomicReference<Device> device2 = new AtomicReference<>(); |
|
|
|
|
|
await(alias + "find device [" + deviceName2 + "] before connecting") |
|
|
|
|
|
.atMost(40, TimeUnit.SECONDS) |
|
|
|
|
|
.until(() -> { |
|
|
|
|
|
device2.set(doGet("/api/tenant/devices?deviceName=" + deviceName2, Device.class)); |
|
|
|
|
|
return device2.get() != null; |
|
|
|
|
|
}); |
|
|
|
|
|
devices.add(device2.get()); |
|
|
|
|
|
Assert.assertEquals(cntDevices, devices.size()); |
|
|
|
|
|
state_ONLINE_ALL (devices, calendar.getTimeInMillis()); |
|
|
|
|
|
// Without full topic: as it was in the old version. When deviceId is updated to full theme, Label is also updated to old deviceId
|
|
|
|
|
|
Assert.assertEquals(deviceIdNameLabel1, device1.get().getLabel()); |
|
|
|
|
|
// // With a full topic: if new. When creating a device by a client to a full topic, if the Label was not filled in - we do not touch it.
|
|
|
|
|
|
Assert.assertNull(device2.get().getLabel()); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Coverage: Rename when a device with the target full-path name already exists (collision). |
|
|
|
|
|
*/ |
|
|
|
|
|
protected void renameCollisionWhenTargetNameAlreadyExists_Test() throws Exception { |
|
|
|
|
|
long ts = calendar.getTimeInMillis(); |
|
|
|
|
|
String shortName = deviceId + "_1"; // Created in beforeTest
|
|
|
|
|
|
String fullPathName = groupId + ":" + edgeNode + ":" + shortName; |
|
|
|
|
|
|
|
|
|
|
|
// Manually create a device that already has the "new" full-path name to trigger a collision
|
|
|
|
|
|
createDevice(fullPathName, deviceProfile.getName(), false); |
|
|
|
|
|
|
|
|
|
|
|
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|
|
|
|
|
|
|
|
|
|
|
SparkplugBProto.Payload.Builder payload = SparkplugBProto.Payload.newBuilder() |
|
|
|
|
|
.setTimestamp(ts) |
|
|
|
|
|
.setSeq(getSeqNum()); |
|
|
|
|
|
payload.addMetrics(createMetric(123, ts, metricBirthName_Int32, metricBirthDataType_Int32, -1L)); |
|
|
|
|
|
|
|
|
|
|
|
// Gateway sends DBIRTH for the short name.
|
|
|
|
|
|
// Transport will try to rename it but should find a conflict and handle it gracefully.
|
|
|
|
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/DBIRTH/" + edgeNode + "/" + shortName, |
|
|
|
|
|
payload.build().toByteArray(), 0, false); |
|
|
|
|
|
|
|
|
|
|
|
await("Checking stability after collision") |
|
|
|
|
|
.atMost(40, TimeUnit.SECONDS) |
|
|
|
|
|
.until(() -> { |
|
|
|
|
|
Device oldDevice = doGet("/api/tenant/devices?deviceName=" + shortName, Device.class); |
|
|
|
|
|
Device conflictDevice = doGet("/api/tenant/devices?deviceName=" + fullPathName, Device.class); |
|
|
|
|
|
// Both devices must still exist, proving no exception crashed the process
|
|
|
|
|
|
return oldDevice != null && conflictDevice != null; |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Coverage: The privilege concern — attempt to rename a device not owned by the gateway. |
|
|
|
|
|
* This test verifies that the original device's ID remains unchanged, meaning it was not hijacked. |
|
|
|
|
|
*/ |
|
|
|
|
|
protected void unauthorizedRenameAttemptBad_Test() throws Exception { |
|
|
|
|
|
long ts = calendar.getTimeInMillis(); |
|
|
|
|
|
String strangerName = "unauthorized_device_rename"; |
|
|
|
|
|
|
|
|
|
|
|
// 1. Create a "stranger" device via API (it has no 'Created' relation to the gateway)
|
|
|
|
|
|
Device stranger = new Device(); |
|
|
|
|
|
stranger.setName(strangerName); |
|
|
|
|
|
stranger.setType("default"); |
|
|
|
|
|
doPost("/api/device", stranger); |
|
|
|
|
|
final DeviceId originalStrangerId = stranger.getId(); |
|
|
|
|
|
|
|
|
|
|
|
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|
|
|
|
|
|
|
|
|
|
|
SparkplugBProto.Payload.Builder payload = SparkplugBProto.Payload.newBuilder() |
|
|
|
|
|
.setTimestamp(ts).setSeq(getSeqNum()); |
|
|
|
|
|
|
|
|
|
|
|
// 2. Unauthorized gateway attempts to rename this device via Sparkplug topic path
|
|
|
|
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/DBIRTH/" + edgeNode + "/" + strangerName, |
|
|
|
|
|
payload.build().toByteArray(), 0, false); |
|
|
|
|
|
|
|
|
|
|
|
String expectedFullPath = groupId + ":" + edgeNode + ":" + strangerName; |
|
|
|
|
|
|
|
|
|
|
|
// 3. Verify security: the original device must still be linked to its short name with the same ID
|
|
|
|
|
|
await("Verify original device was not hijacked") |
|
|
|
|
|
.atMost(40, TimeUnit.SECONDS) |
|
|
|
|
|
.pollDelay(2, TimeUnit.SECONDS) |
|
|
|
|
|
.untilAsserted(() -> { |
|
|
|
|
|
// Check if the original device still exists with its original ID
|
|
|
|
|
|
Device currentStranger = doGet("/api/tenant/devices?deviceName=" + strangerName, Device.class); |
|
|
|
|
|
Assert.assertNotNull("Original device disappeared!", currentStranger); |
|
|
|
|
|
Assert.assertEquals("Security breach: Original device ID changed!", originalStrangerId, currentStranger.getId()); |
|
|
|
|
|
|
|
|
|
|
|
// Even if the gateway created a NEW device with a full path, it must have a different ID
|
|
|
|
|
|
Device newDevice = doGet("/api/tenant/devices?deviceName=" + expectedFullPath, Device.class); |
|
|
|
|
|
if (newDevice != null) { |
|
|
|
|
|
Assert.assertNotEquals("Stranger device was successfully hijacked (IDs match)!", originalStrangerId, newDevice.getId()); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Coverage: The privilege concern — attempt to rename a device not owned by the gateway. |
|
|
|
|
|
*/ |
|
|
|
|
|
protected void unauthorizedRenameAttempt_Test() throws Exception { |
|
|
|
|
|
long ts = calendar.getTimeInMillis(); |
|
|
|
|
|
String strangerName = "unauthorized_device_rename"; |
|
|
|
|
|
|
|
|
|
|
|
// Create a device without a "Created" relation to the gateway
|
|
|
|
|
|
Device stranger = new Device(); |
|
|
|
|
|
stranger.setName(strangerName); |
|
|
|
|
|
stranger.setType("default"); |
|
|
|
|
|
doPost("/api/device", stranger); |
|
|
|
|
|
|
|
|
|
|
|
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|
|
|
|
|
|
|
|
|
|
|
SparkplugBProto.Payload.Builder payload = SparkplugBProto.Payload.newBuilder() |
|
|
|
|
|
.setTimestamp(ts).setSeq(getSeqNum()); |
|
|
|
|
|
|
|
|
|
|
|
// Unauthorized gateway attempts to rename the device via Sparkplug topic
|
|
|
|
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/DBIRTH/" + edgeNode + "/" + strangerName, |
|
|
|
|
|
payload.build().toByteArray(), 0, false); |
|
|
|
|
|
|
|
|
|
|
|
String expectedFullPath = groupId + ":" + edgeNode + ":" + strangerName; |
|
|
|
|
|
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> |
|
|
|
|
|
doGet("/api/tenant/devices?deviceName=" + expectedFullPath, Device.class, status().isNotFound()) |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
protected void state_ONLINE_ALL (List<Device> devices, long ts) { |
|
|
|
|
|
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), ONLINE.name())); |
|
|
|
|
|
await(alias + messageName(STATE) + ", device: " + savedGateway.getName()) |
|
|
|
|
|
.atMost(40, TimeUnit.SECONDS) |
|
|
|
|
|
.until(() -> { |
|
|
|
|
|
var foundEntry = tsService.findAllLatest(tenantId, savedGateway.getId()).get().stream() |
|
|
|
|
|
.filter(tsKv -> tsKv.getKey().equals(tsKvEntry.getKey())) |
|
|
|
|
|
.filter(tsKv -> tsKv.getValue().equals(tsKvEntry.getValue())) |
|
|
|
|
|
.filter(tsKv -> tsKv.getTs() == tsKvEntry.getTs()) |
|
|
|
|
|
.findFirst(); |
|
|
|
|
|
return foundEntry.isPresent(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
for (Device device : devices) { |
|
|
|
|
|
await(alias + messageName(STATE) + ", device: " + device.getName()) |
|
|
|
|
|
.atMost(40, TimeUnit.SECONDS) |
|
|
|
|
|
.until(() -> { |
|
|
|
|
|
var foundEntry = tsService.findAllLatest(tenantId, device.getId()).get().stream() |
|
|
|
|
|
.filter(tsKv -> tsKv.getKey().equals(tsKvEntry.getKey())) |
|
|
|
|
|
.filter(tsKv -> tsKv.getValue().equals(tsKvEntry.getValue())) |
|
|
|
|
|
.filter(tsKv -> tsKv.getTs() == tsKvEntry.getTs()) |
|
|
|
|
|
.findFirst(); |
|
|
|
|
|
return foundEntry.isPresent(); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Coverage: Concurrent first-message registration with the lock mechanism. |
|
|
|
|
|
*/ |
|
|
|
|
|
protected void concurrentFirstMessageRegistration_Test() throws Exception { |
|
|
|
|
|
int threadCount = 5; |
|
|
|
|
|
String concurrentDeviceName = "concurrent_device"; |
|
|
|
|
|
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|
|
|
|
|
|
|
|
|
|
|
java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(threadCount); |
|
|
|
|
|
long ts = calendar.getTimeInMillis(); |
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < threadCount; i++) { |
|
|
|
|
|
executor.submit(() -> { |
|
|
|
|
|
try { |
|
|
|
|
|
SparkplugBProto.Payload.Builder payload = SparkplugBProto.Payload.newBuilder() |
|
|
|
|
|
.setTimestamp(ts).setSeq(0); |
|
|
|
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/DBIRTH/" + edgeNode + "/" + concurrentDeviceName, |
|
|
|
|
|
payload.build().toByteArray(), 0, false); |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
log.error("Concurrent publish failed", e); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
String expectedName = groupId + ":" + edgeNode + ":" + concurrentDeviceName; |
|
|
|
|
|
await("Wait for concurrent registration result") |
|
|
|
|
|
.atMost(40, TimeUnit.SECONDS) // Restored to 40s as requested
|
|
|
|
|
|
.until(() -> doGet("/api/tenant/devices?deviceName=" + expectedName, Device.class) != null); |
|
|
|
|
|
|
|
|
|
|
|
executor.shutdown(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Coverage: Sparkplug-message handling when msgId <= 0 (#7). |
|
|
|
|
|
* Verifies that the transport does not close the session for Sparkplug clients using msgId 0. |
|
|
|
|
|
*/ |
|
|
|
|
|
protected void sparkplugSessionStaysAliveWithZeroMsgId_Test() throws Exception { |
|
|
|
|
|
// clientMqttV5ConnectWithNDEATH internally sets msgId = 0 for the Will message.
|
|
|
|
|
|
// This validates that the connection is accepted despite msgId being 0.
|
|
|
|
|
|
IMqttToken connectionResult = clientMqttV5ConnectWithNDEATH(calendar.getTimeInMillis(), 0, -1L); |
|
|
|
|
|
Assert.assertTrue("Sparkplug connection should be successful with msgId=0", client.isConnected()); |
|
|
|
|
|
|
|
|
|
|
|
// Publish NBIRTH message which usually goes through the aggregate callback.
|
|
|
|
|
|
// This verifies that msgId=0 in the callback does not trigger closeDeviceSession.
|
|
|
|
|
|
connectionWithNBirth(Int32, "test_metric_msgId_0", 555); |
|
|
|
|
|
|
|
|
|
|
|
// Awaitility to ensure the session remains open after processing.
|
|
|
|
|
|
await("Verify Sparkplug session remains open after receiving msgId=0") |
|
|
|
|
|
.atMost(40, TimeUnit.SECONDS) |
|
|
|
|
|
.until(() -> client.isConnected()); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
protected List<Device> connectClientWithCorrectAccessTokenWithNDEATHWithAliasCreatedDevices(long ts) throws Exception { |
|
|
protected List<Device> connectClientWithCorrectAccessTokenWithNDEATHWithAliasCreatedDevices(long ts) throws Exception { |
|
|
List<Device> devices = new ArrayList<>(); |
|
|
List<Device> devices = new ArrayList<>(); |
|
|
Long alias = 0L; |
|
|
Long alias = 0L; |
|
|
@ -219,7 +496,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte |
|
|
payloadBirthNode.addMetrics(metric); |
|
|
payloadBirthNode.addMetrics(metric); |
|
|
payloadBirthNode.setTimestamp(ts); |
|
|
payloadBirthNode.setTimestamp(ts); |
|
|
if (client.isConnected()) { |
|
|
if (client.isConnected()) { |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.NBIRTH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode, |
|
|
payloadBirthNode.build().toByteArray(), 0, false); |
|
|
payloadBirthNode.build().toByteArray(), 0, false); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -228,15 +505,16 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte |
|
|
SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() |
|
|
SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() |
|
|
.setTimestamp(ts) |
|
|
.setTimestamp(ts) |
|
|
.setSeq(getSeqNum()); |
|
|
.setSeq(getSeqNum()); |
|
|
String deviceName = deviceId + "_" + 1; |
|
|
String deviceIdName = deviceId + "_1"; |
|
|
|
|
|
String deviceName = groupId + ":" + edgeNode + ":" + deviceIdName; |
|
|
|
|
|
|
|
|
payloadBirthDevice.addMetrics(metric); |
|
|
payloadBirthDevice.addMetrics(metric); |
|
|
if (client.isConnected()) { |
|
|
if (client.isConnected()) { |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName, |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.DBIRTH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode + TOPIC_SPLIT_SEPARATOR + deviceIdName, |
|
|
payloadBirthDevice.build().toByteArray(), 0, false); |
|
|
payloadBirthDevice.build().toByteArray(), 0, false); |
|
|
AtomicReference<Device> device = new AtomicReference<>(); |
|
|
AtomicReference<Device> device = new AtomicReference<>(); |
|
|
await(alias + "find device [" + deviceName + "] after created") |
|
|
await(alias + "find device [" + deviceName + "] after created") |
|
|
.atMost(200, TimeUnit.SECONDS) |
|
|
.atMost(40, TimeUnit.SECONDS) |
|
|
.ignoreExceptions() |
|
|
.ignoreExceptions() |
|
|
.until(() -> { |
|
|
.until(() -> { |
|
|
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); |
|
|
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); |
|
|
@ -282,7 +560,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte |
|
|
listKeys.add(metricKey); |
|
|
listKeys.add(metricKey); |
|
|
|
|
|
|
|
|
if (client.isConnected()) { |
|
|
if (client.isConnected()) { |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, |
|
|
client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.NBIRTH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode, |
|
|
payloadBirthNode.build().toByteArray(), 0, false); |
|
|
payloadBirthNode.build().toByteArray(), 0, false); |
|
|
} |
|
|
} |
|
|
return listKeys; |
|
|
return listKeys; |
|
|
@ -525,4 +803,9 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) { |
|
|
|
|
|
return new EntityRelation(mainDevice.getId(), device.getId(), relationType); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|